Download as pdf or txt
Download as pdf or txt
You are on page 1of 105

CloudEngine 12800 Series Switches

Configuration Guide - Basic Configuration 2 ZTP Configuration

2 ZTP Configuration

This chapter describes how to configure Zero Touch Provisioning (ZTP) to allow
automatic deployment of unconfigured devices after power-on.

2.1 Overview of ZTP


2.2 Understanding ZTP
2.3 Licensing Requirements and Limitations for ZTP
2.4 Default Settings for ZTP
2.5 Configuring an Unconfigured Device to Implement Automatic Deployment
Using a USB Flash Drive
2.6 Configuring an Unconfigured Device to Implement Automatic Deployment
Through DHCP
2.7 Configuring a DHCP Client
2.8 Configuration Examples for ZTP
This section provides ZTP configuration examples, including the networking
requirements, configuration roadmap, and configuration procedure.

2.1 Overview of ZTP

Definition
Zero Touch Provisioning (ZTP) allows newly delivered or unconfigured devices to
automatically load version files, including system software, license files, user-
defined files, configuration files, and patch files, after they start.

Purpose
Typically, after devices are installed, administrators have to commission the
software onsite. If a large number of devices are sparsely distributed across a
network, manually configuring these device results in poor device deployment
efficiency and high labor costs.
These issues can be eradicated using ZTP. Devices running ZTP can automatically
obtain and load version files from a USB flash drive or file server, allowing the

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 27


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

administrator to configure and deploy devices without physically being onsite. In


this way, ZTP reduces labor costs and increases device deployment efficiency.

Related Documents
Video: CloudEngine Series Switches ZTP Introduction

2.2 Understanding ZTP

2.2.1 Implementation

Automatic Deployment and Typical Networking


After an unconfigured device is powered on, the device automatically starts the
ZTP process. The device will attempt to implement automatic deployment through
a USB flash drive and then through DHCP upon a USB-based deployment failure.
For the detailed process, see ZTP Process.
Figure 2-1 shows the typical networking of automatic deployment through DHCP.

Figure 2-1 Networking diagram of automatic deployment

● DHCP server: assigns a temporary management IP address, default gateway


address, DNS server address, and intermediate file server address to the
device running ZTP.
● Syslog server: uploads user logs recorded during the ZTP process to the NMS.
● DHCP relay agent: relays packets exchanged between the device running ZTP
and DHCP server located on different network segments.
● Intermediate file server: stores the intermediate file required by the device
running ZTP. The intermediate file can be an .ini file or a Python script. By

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 28


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

parsing the intermediate file, the device running ZTP obtains information
about the version files and version file server address. The intermediate file
server can be a TFTP, FTP, or SFTP server.
● Version file server: stores the version files to be loaded to unconfigured
devices, including the system software, configuration files, and patch files. The
version file server and intermediate file server can be deployed on the same
file server, which can be a TFTP, FTP, or SFTP server.
● DNS server: provides mappings between domain names and IP addresses,
and resolves the file server domain name to an IP address for the ZTP-running
device. Based on the resolved IP address, the device can obtain requested files
from the file server.

NOTE

TFTP and FTP pose security risks, and SFTP is recommended for file transfer.

ZTP Process
Figure 2-2 shows the ZTP flowchart.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 29


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Figure 2-2 ZTP flowchart

The ZTP process goes through six stages:


1. The device is powered on and starts.
If a configuration file is available, the device starts with the configuration file.
If no configuration file is available, the device automatically starts the ZTP
process.
If you use a console port to log in when the device is powered on, you can
choose whether to terminate the ZTP process as prompted. If you choose to
terminate the ZTP process, the device starts with no configuration.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 30


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

If two MPUs are installed, you can only log in and check ZTP information through the
console port of the active MPU. If you log in to the standby MPU, the system displays a
message indicating that the other MPU is executing the ZTP process. Before starting the
ZTP process, you need to connect the two MPUs to their respective console ports.
2. The device obtains the intermediate file and version files from the USB flash
drive.
After the ZTP process starts, the unconfigured device first tries to obtain the
intermediate file from the USB flash drive. If the device obtains the
intermediate file, it parses the file and gets information about the version files
to be loaded. After downloading the version files, the device restarts to
complete automatic deployment. The device enters stage 3 if any of the
following conditions occur: no USB flash drive is installed; the USB flash drive
contains an incorrect intermediate file; the device fails to obtain the version
files.
3. The device obtains DHCP information.
If the device fails to implement automatic deployment using the USB flash
drive, it starts automatic deployment through DHCP. In this mode, the device
broadcasts DHCP Request packets on ports in the following sequence:
management Ethernet port -> high-bandwidth Ethernet port -> low-
bandwidth Ethernet port. After receiving the DHCP Request packet, the DHCP
server sends a DHCP Reply packet to the device. Options in the packet contain
the device-requested information, including the IP address for the device,
default gateway, and IP address of the intermediate file server, IP address of
the Syslog server, and name of the intermediate file.
4. The Syslog server is enabled.
The device obtains the IPv4 address of the Syslog server from the DHCP Reply
packet, that is, the Syslog server is enabled. During ZTP startup, important
phase information is recorded in user logs. The Syslog server will upload the
user logs to the NMS.
5. The device obtains the intermediate file and version files.
The device uses the information carried in the DHCP Reply packet to obtain
the intermediate file and then download the version files from the version file
server.
If the intermediate file is an .ini file, the device downloads the version files
based on the IP address and version file names contained in the intermediate
file. If the intermediate file is a Python script, the device automatically runs
the script to download the version files.
6. The device restarts.
The device automatically sets the version files downloaded from the server as
the next startup files. Then the device restarts, and automatic deployment is
complete.

Implementing ZTP Through iMaster NCE-Fabric


NOTE

To implement ZTP through iMaster NCE-Fabric, ensure that the iMaster NCE-Fabric version is
V300R002C10 or later.

Figure 2-3 shows how ZTP is implemented through iMaster NCE-Fabric.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 31


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Figure 2-3 Implementing ZTP through iMaster NCE-Fabric

● Root device: a device that has been connected to iMaster NCE-Fabric and is
used to connect to unconfigured devices. An authentication certificate has
been preconfigured on the device so that the device can establish a NETCONF
connection with iMaster NCE-Fabric through certificate authentication.
● iMaster NCE-Fabric: is used for ZTP and connects to unconfigured devices
after ZTP is complete. An authentication certificate has been preconfigured on
iMaster NCE-Fabric so that Agile Controller-DCN can establish a NETCONF
connection with the root device through certificate authentication.

The following describes the process for implementing ZTP through iMaster NCE-
Fabric:
1. An administrator installs iMaster NCE-Fabric, DHCP server, intermediate file
server, and version file server and physically connects these servers to the root
device.
2. An unconfigured device starts after power-on and broadcasts a DHCP Request
packet on its management interface and Ethernet interface. After receiving
the DHCP Request packet, the DHCP server sends a DHCP Reply packet to the
device. Options in the packet contain the device-requested information,
including the temporary IP address for the device, IP address of the
intermediate file server, user name and password for logging in to the

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 32


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

directory where the boot.py file is stored, and path as well as name of the
script file boot.py.
3. The device downloads the script file boot.py, executes this file, enables the
LLDP function, generates and executes the callhome configuration, and uses
the temporary IP address to register with iMaster NCE-Fabric.
4. After the authentication succeeds, the device establishes a NETCONF
connection with iMaster NCE-Fabric. The device then obtains a topology file
from iMaster NCE-Fabric and obtains required version files from the version
file server.
5. The device automatically sets the version files downloaded from the server as
the next startup files. Then the device restarts, and automatic deployment is
complete.
6. After the device restarts, it uses its management IP address to register with
iMaster NCE-Fabric. After the authentication succeeds, iMaster NCE-Fabric
manages this device.

2.2.2 Intermediate File in INI Format


An .ini file can be used as an intermediate file to store device and version file
information.
The .ini file has the name extension .ini. The file format is as follows:

NOTE

● If the device implements automatic deployment using a USB flash drive, the name of the .ini
file must be ztp_config.ini. If the device implements automatic deployment through DHCP, a
user-defined file name can be used.
● The configured user name, password, and version file name in the intermediate file cannot
contain special characters, including & > < " ' / #.
● The .ini file must be in ANSI encoding format.
● Stacking is not configured:
;BEGIN DC
[GLOBAL CONFIG]
FILESERVER=sftp://sftpuser:Pwd123456@10.1.3.2

[DEVICEn DESCRIPTION]
ESN=210235527210D4000028
MAC=e468-a356-0cb0
DEVICETYPE=DEFAULT
SYSTEM-SOFTWARE=CE12800-V100R005C00SPC300.cc
SYSTEM-CONFIG=vrpcfg.cfg
SYSTEM-PAT=CE12800-V100R005SPH001.PAT
;END DC

● Stacking is configured:
;BEGIN DC
[GLOBAL CONFIG]
FILESERVER=sftp://sftpuser:Pwd123456@10.1.3.2

[DEVICE0 DESCRIPTION]
ESN=210235527210D4000028
MAC=e468-a356-0cb0
DEVICETYPE=DEFAULT
SYSTEM-SOFTWARE=CE12800-V100R005C00SPC300.cc
SYSTEM-CONFIG=vrpcfg.cfg
SYSTEM-PAT=CE12800-V100R005SPH001.PAT

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 33


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

STACK-MEMBER-ID=2
[DEVICE1 DESCRIPTION]
ESN=210235527210D4000029
MAC=0025-9e01-0203
DEVICETYPE=DEFAULT
SYSTEM-SOFTWARE=CE12800-V100R005C00SPC300.cc
SYSTEM-CONFIG=vrpcfg.cfg
SYSTEM-PAT=CE12800-V100R005SPH001.PAT
STACK-MEMBER-ID=1
;END DC

Table 2-1 Fields in the .ini file


Field Mandatory or Description
Optional

;BEGIN DC Mandatory The start flag of the file. This field


cannot be modified.

[GLOBAL CONFIG] Mandatory The start flag of the global


configuration. This field cannot be
modified.

FILESERVER Mandatory If a USB flash drive is used for


automatic deployment, this field is
in the following format:
● file:/usb:/path
path specifies the directory where
version files are saved in the USB
flash drive.
If automatic deployment is
performed through DHCP, this field
specifies the server path where the
device can obtain version files. The
version file server can be a TFTP, an
FTP, SFTP server. The format of this
field depends on the server type and
can be any of the following:
● tftp://hostname/path
● ftp://
[username[:password]@]hostna
me[:port]/path
● sftp://
[username[:password]@]hostna
me[:port]/path
The parameters username,
password, and port are optional.
path specifies the directory where
the version file is saved on the file
server.
NOTE
TFTP, FTP pose security risks. The SFTP
mode is recommended.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 34


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Field Mandatory or Description


Optional

[DEVICEn Mandatory The start flag of the file description.


DESCRIPTION] n is a device number, which is an
integer and starts from 0.

ESN Optional Specifies the equipment serial


number (ESN) of a device. If this
field is set to DEFAULT, the ESN of
the device is not checked. If this
field is set to another value, the ESN
of the device must be the same as
the configured value.
The default value of this field is
DEFAULT. If this field does not exist
or is empty, the default value is
used.

MAC Optional Specifies the MAC address of a


device, in the XXXX-XXXX-XXXX
format, where X is a hexadecimal
number. If this field is set to
DEFAULT, the device MAC address is
not checked. If this field is set to
another value, the device MAC
address must be the same as the
configured value.
The device ESN is checked ahead of
the MAC address.
The default value of this field is
DEFAULT. If this field does not exist
or is empty, the default value is
used.

DEVICETYPE Optional Specifies the device type. The value


of this field can be CE12800. If this
field is set to DEFAULT, the device
type is not checked.
The default value of this field is
DEFAULT. If this field does not exist
or is empty, the default value is
used.

SYSTEM- Optional Specifies a system software name,


SOFTWARE with an extension .cc. This field
cannot be left blank. If the system
software name does not need to be
specified, delete this field.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 35


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Field Mandatory or Description


Optional

SYSTEM-CONFIG Optional Specifies a configuration file name,


with an extension .cfg, .zip, or .dat.
This field cannot be left blank. If the
configuration file name does not
need to be specified, delete this
field.

SYSTEM-PAT Optional Specifies a patch file name, with an


extension .pat. If the patch file name
does not need to be specified, delete
this field.

STACK-MEMBER-ID Optional Specifies the ID of a stack member


switch. The value is 1 or 2.
If this flag does not exist or is
empty, stacking is not configured on
the device.

;END DC Mandatory The end flag of the file. This field


cannot be modified.

2.2.3 Intermediate File in Python Format


A Python script can be used as an intermediate file. The device runs the Python
script to download version files.
The file name extension of the Python script must be .py. Example of a Python
Script File shows an example. Python Script Description describes the contents
of the Python script.

NOTE

● If the device implements automatic deployment using a USB flash drive, the name of the
Python script must be ztp_script.py. If the device implements automatic deployment
through DHCP, a user-defined file name can be used.
● The configured user name, password, and version file name in the intermediate file cannot
contain special characters, including & > < " ' / #.

Example of a Python Script File


NOTE

This script file is used for reference only and can be transferred using SFTP. You can modify the
script file based on deployment requirements.
#sha256sum="7eb57f7cf11d2bf481e544f333267e8dd43a532c12a60151d640d64872013a8b"
#!/usr/bin/env python
#
# Copyright (C) Huawei Technologies Co., Ltd. 2008-2013. All rights reserved.
# ----------------------------------------------------------------------------------------------------------------------
# History:
# Date Author Modification

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 36


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# 20130629 Author created file.


# ----------------------------------------------------------------------------------------------------------------------

"""
Zero Touch Provisioning (ZTP) enables devices to automatically load version files including system software,
patch files, configuration files when the device starts up, the devices to be configured must be new devices
or have no configuration files.

This is a sample of Zero Touch Provisioning user script. You can customize it to meet the requirements of
your network environment.
"""

import http.client
import urllib.request, urllib.parse, urllib.error
import string
import re
import xml.etree.ElementTree as etree
import os
import stat
import logging
import traceback
import hashlib
import sys

from urllib.parse import urlparse


from urllib.parse import urlunparse
from time import sleep

# error code
OK =0
ERR =1

# File server in which stores the necessary system software, configuration and patch files:
# 1) Specify the file server which supports the following format.
# sftp://[username[:password]@]hostname[:port]
# 2) Do not add a trailing slash at the end of file server path.
FILE_SERVER = 'sftp://sftpuser:Pwd123@10.1.3.2'

# Remote file paths:


# 1) The path may include directory name and file name.
# 2) If file name is not specified, indicate the procedure can be skipped.
# File paths of system software on file server, filename extension is '.cc'.
REMOTE_PATH_IMAGE = { 'CE12800' : '/image/CE6800-V200R019C10.cc',
}
# File path of configuration file on file server, filename extension is '.cfg', '.zip' or '.dat'.
REMOTE_PATH_CONFIG = '/config/conf_%s.cfg'
# File path of patch file on file server, filename extension is '.pat'
REMOTE_PATH_PATCH = {
'CE12800' : '/patch/CE12800-V200R001SPH001.PAT',
}
# File path of stack member ID file on file server, filename extension is '.txt'
REMOTE_PATH_MEMID = '/stack/stack_member.txt'
# File path of license list file, filename extension is '.xml'
REMOTE_PATH_LICLIST = 'Index.xml'
# File path of sha256 file, contains sha256 value of image / patch / memid / license file, file extension is
'.txt'
REMOTE_PATH_SHA256 = '/sha256.txt'
# File path of python file on file server, filename extension is '.py'
REMOTE_PATH_PYTHON = '/get_systeminfo.py'

# Max times to retry get startup when no query result


GET_STARTUP_INTERVAL = 15 # seconds
MAX_TIMES_GET_STARTUP = 120 # Max times to retry

# Max times to retry when download file faild


MAX_TIMES_RETRY_DOWNLOAD = 3

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 37


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

class OPSConnection(object):
"""Make an OPS connection instance."""

def __init__(self, host, port = 80):


self.host = host
self.port = port
self.headers = {
"Content-type": "application/xml",
"Accept": "application/xml"
}

self.conn = http.client.HTTPConnection(self.host, self.port)

def close(self):
"""Close the connection"""
self.conn.close()

def create(self, uri, req_data):


"""Create a resource on the server"""
ret = self._rest_call("POST", uri, req_data)
return ret

def delete(self, uri, req_data):


"""Delete a resource on the server"""
ret = self._rest_call("DELETE", uri, req_data)
return ret

def get(self, uri, req_data = None):


"""Retrieve a resource from the server"""
ret = self._rest_call("GET", uri, req_data)
return ret

def set(self, uri, req_data):


"""Update a resource on the server"""
ret = self._rest_call("PUT", uri, req_data)
return ret

def _rest_call(self, method, uri, req_data):


"""REST call"""
if req_data == None:
body = ""
else:
body = req_data

logging.info('HTTP request: %s %s HTTP/1.1', method, uri)


self.conn.request(method, uri, body, self.headers)
response = self.conn.getresponse()
ret = (response.status, response.reason, response.read())
if response.status != http.client.OK:
logging.info('%s', body)
logging.error('HTTP response: HTTP/1.1 %s %s\n%s', ret[0], ret[1], ret[2])
return ret

class OPIExecError(Exception):
"""OPI executes error."""
pass

class ZTPErr(Exception):
"""ZTP error."""
pass

def get_addr_by_hostname(ops_conn, host, addr_type = '1'):


"""Translate a host name to IPv4 address format. The IPv4 address is returned as a string."""
logging.info("Get IP address by host name...")
uri = "/dns/dnsNameResolution"
root_elem = etree.Element('dnsNameResolution')
etree.SubElement(root_elem, 'host').text = host
etree.SubElement(root_elem, 'addrType').text = addr_type

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 38


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

req_data = etree.tostring(root_elem, "UTF-8")


ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to get address by host name')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "ipv4Addr", namespaces)
if elem is None:
raise OPIExecError('Failed to get IP address by host name')

return elem.text

def _del_rsa_peer_key(ops_conn, key_name):


"""Delete RSA peer key configuration"""
logging.info("Delete RSA peer key %s", key_name)
uri = "/rsa/rsaPeerKeys/rsaPeerKey"
root_elem = etree.Element('rsaPeerKey')
etree.SubElement(root_elem, 'keyName').text = key_name
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete RSA peer key')

except Exception as reason:


logging.error(reason)

def _del_sshc_rsa_key(ops_conn, server_name, key_type = 'RSA'):


"""Delete SSH client RSA key configuration"""
logging.debug("Delete SSH client RSA key for %s", server_name)
uri = "/sshc/sshCliKeyCfgs/sshCliKeyCfg"
root_elem = etree.Element('sshCliKeyCfg')
etree.SubElement(root_elem, 'serverName').text = server_name
etree.SubElement(root_elem, 'pubKeyType').text = key_type
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete SSH client RSA key')

except Exception as reason:


logging.error(reason)

_del_rsa_peer_key(ops_conn, server_name)

def _set_sshc_first_time(ops_conn, switch):


"""Set SSH client attribute of authenticating user for the first time access"""
if switch not in ['Enable', 'Disable']:
return ERR

logging.info('Set SSH client first-time enable switch = %s', switch)


uri = "/sshc/sshClient"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshClient>
<firstTimeEnable>$enable</firstTimeEnable>
</sshClient>
''')
req_data = str_temp.substitute(enable = switch)
ret, _, _ = ops_conn.set(uri, req_data)
if ret != http.client.OK:
if switch == 'Enable':
raise OPIExecError('Failed to enable SSH client first-time')
else:
raise OPIExecError('Failed to disable SSH client first-time')

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 39


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

return OK

def _sftp_download_file(ops_conn, url, local_path):


"""Download file using SFTP."""
_set_sshc_first_time(ops_conn, 'Enable')

logging.info('SFTP download "%s" to "%s".', url, local_path)


uri = "/sshc/sshcConnects/sshcConnect"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshcConnect>
<HostAddrIPv4>$serverIp</HostAddrIPv4>
<commandType>get</commandType>
<userName>$username</userName>
<password>$password</password>
<localFileName>$localPath</localFileName>
<remoteFileName>$remotePath</remoteFileName>
<identityKey>ssh-rsa</identityKey>
<transferType>SFTP</transferType>
</sshcConnect>
''')
url_tuple = urlparse(url)
if re.match(r"\d+\.\d+\.\d+\.\d+", url_tuple.hostname):
server_ip = url_tuple.hostname
else:
server_ip = get_addr_by_hostname(ops_conn, url_tuple.hostname)
req_data = str_temp.substitute(serverIp = server_ip, username = url_tuple.username, password =
url_tuple.password,
remotePath = url_tuple.path[1:], localPath = local_path)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
print(('Failed to download file "%s" using SFTP' % os.path.basename(local_path)))
sys.stdout.flush()
ret = ERR
else:
ret = OK

_del_sshc_rsa_key(ops_conn, server_ip)
_set_sshc_first_time(ops_conn, 'Disable')
return ret

def _usb_download_file(ops_conn, url, local_path):


"""Download file using usb"""
logging.info('USB download "%s" to "%s".', url, local_path)

url_tuple = urlparse(url, allow_fragments=False)


src_path = url_tuple.path[1:]
try:
copy_file(ops_conn, src_path, local_path)
except:
print(('Failed to download file "%s" using USB' % os.path.basename(local_path)))
sys.stdout.flush()
return ERR
return OK

def download_file(ops_conn, url, local_path, retry_times = 0):


"""Download file, support SFTP.

sftp://[username[:password]@]hostname[:port]/path

Args:
ops_conn: OPS connection instance
url: URL of remote file
local_path: local path to put the file

Returns:
A integer of return code
"""
url_tuple = urlparse(url)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 40


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

print(("Info: Download %s to %s" % (url_tuple.path[1:], local_path)))


sys.stdout.flush()
func_dict = {'sftp': _sftp_download_file,
'file': _usb_download_file}
scheme = url_tuple.scheme
if scheme not in list(func_dict.keys()):
raise ZTPErr('Unknown file transfer scheme %s' % scheme)

ret = OK
cnt = 0
while (cnt < 1 + retry_times):
if cnt:
print('Retry downloading...')
sys.stdout.flush()
logging.info('Retry downloading...')
ret = func_dict[scheme](ops_conn, url, local_path)
if ret is OK:
break
cnt += 1

if ret is not OK:


raise ZTPErr('Failed to download file "%s"' % os.path.basename(url))

return OK

class StartupInfo(object):
"""Startup configuration information

image: startup system software


config: startup saved-configuration file
patch: startup patch package
"""
def __init__(self, image = None, config = None, patch = None):
self.image = image
self.config = config
self.patch = patch

class Startup(object):
"""Startup configuration information

current: current startup configuration


next: current next startup configuration
"""
def __init__(self, ops_conn):
self.ops_conn = ops_conn
self.current, self.next = self._get_startup_info()

def _get_startup_info(self):
"""Get the startup information."""
logging.info("Get the startup information...")
uri = "/cfg/startupInfos/startupInfo"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<startupInfo>
<position/>
<configedSysSoft/>
<curSysSoft/>
<nextSysSoft/>
<curStartupFile/>
<nextStartupFile/>
<curPatchFile/>
<nextPatchFile/>
</startupInfo>'''

cnt = 0
while (cnt < MAX_TIMES_GET_STARTUP):
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
cnt += 1

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 41


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

logging.warning('Failed to get the startup information')


continue

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
mpath = 'data' + uri.replace('/', '/vrp:') # match path
nslen = len(namespaces['vrp'])
elem = root_elem.find(mpath, namespaces)
if elem is not None:
break
logging.warning('No query result while getting startup info')
sleep(GET_STARTUP_INTERVAL) # sleep to wait for system ready when no query result
cnt += 1

if elem is None:
raise OPIExecError('Failed to get the startup information')

current = StartupInfo() # current startup info


curnext = StartupInfo() # next startup info
for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}text'
if tag == 'curSysSoft':
current.image = child.text
elif tag == 'nextSysSoft':
curnext.image = child.text
elif tag == 'curStartupFile' and child.text != 'NULL':
current.config = child.text
elif tag == 'nextStartupFile' and child.text != 'NULL':
curnext.config = child.text
elif tag == 'curPatchFile' and child.text != 'NULL':
current.patch = child.text
elif tag == 'nextPatchFile' and child.text != 'NULL':
curnext.patch = child.text
else:
continue

return current, curnext

def _set_startup_image_file(self, file_path):


"""Set the next startup system software"""
logging.info("Set the next startup system software to %s...", file_path)
uri = "/sum/startupbymode"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startupbymode>
<softwareName>$fileName</softwareName>
<mode>STARTUP_MODE_ALL</mode>
</startupbymode>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup system software")

def _set_startup_config_file(self, file_path):


"""Set the next startup saved-configuration file"""
logging.info("Set the next startup saved-configuration file to %s...", file_path)
uri = "/cfg/setStartup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<setStartup>
<fileName>$fileName</fileName>
</setStartup>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 42


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

raise OPIExecError("Failed to set startup configuration file")

def _del_startup_config_file(self):
"""Delete startup config file"""
logging.info("Delete the next startup config file...")
uri = "/cfg/clearStartup"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<clearStartup>
</clearStartup>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to delete startup configuration file")

def _set_startup_patch_file(self, file_path):


"""Set the next startup patch file"""
logging.info("Set the next startup patch file to %s...", file_path)
uri = "/patch/startup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startup>
<packageName>$fileName</packageName>
</startup>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup patch file")

def _get_cur_stack_member_id(self):
"""rest api: Get current stack member id"""

logging.info("Get current stack member ID...")


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID></memberID>
</stackMemberInfo>
'''
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get current stack member id, rsp not ok')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "memberID", namespaces)
if elem is None:
raise OPIExecError('Failed to get the current stack member id for no "memberID" element')

return elem.text

def _set_stack_member_id(self, file_path, esn):


"""Set the next stack member ID"""

def get_stackid_from_file(fname, esn):


"""parse esn_id.txt file and get stack id according to esn num
format of esn_stackid file is like below:

sn Irf group Irf number


Sdddg 100 1
Sddde 100 2
"""
# fname must exist, guaranteed by caller
fname = os.path.basename(fname)
with open(fname, 'r') as item:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 43


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

for line in item:


token = line.strip('[\r\n]')
token = token.split()
if token[0] == esn:
return token[2]
return None

logging.info('Set the next stack member ID, filename %s', file_path)


uri = "/stack/stackMemberInfos/stackMemberInfo"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>$curmemberid</memberID>
<nextMemberID>$memberid</nextMemberID>
</stackMemberInfo>
''')

cur_memid = self._get_cur_stack_member_id()
next_memid = get_stackid_from_file(file_path, esn)
if not next_memid:
logging.error('Failed to get stack id from %s, esn %s', file_path, esn)
return

req_data = str_temp.substitute(curmemberid = cur_memid, memberid = next_memid)


ret, _, _ = self.ops_conn.set(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to set stack id {}'.format(next_memid))

return OK

def _reset_stack_member_id(self):
"""rest api: reset stack member id"""

logging.info('Reset the next stack member ID')


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>1</memberID>
<nextMemberID>1</nextMemberID>
</stackMemberInfo>
'''

ret, _, _ = self.ops_conn.set(uri, req_data)


if ret != http.client.OK:
raise OPIExecError('Failed to reset stack id ')

return OK

def _reset_startup_patch_file(self):
"""Rest patch file for system to startup"""
logging.info("Reset the next startup patch file...")
uri = "/patch/resetpatch"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<resetpatch>
</resetpatch>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to reset patch')

def reset_startup_info(self, slave):


"""Reset startup info and delete the downloaded files"""
logging.info("Reset the next startup information...")
_, configured = self._get_startup_info()

# 1. Reset next startup config file and delete it


try:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 44


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if configured.config != self.next.config:
if self.next.config is None:
self._del_startup_config_file()
else:
self._set_startup_config_file(self.next.config)
if configured.config is not None:
del_file_all(self.ops_conn, configured.config, slave)

except Exception as reason:


logging.error(reason)

# 2. Reset next startup patch file


try:
if configured.patch != self.next.patch:
if self.next.patch is None:
self._reset_startup_patch_file()
else:
self._set_startup_patch_file(self.next.patch)

if configured.patch is not None:


del_file_all(self.ops_conn, configured.patch, slave)
except Exception as reason:
logging.error(reason)

# 3. Reset next startup system software and delete it


try:
if configured.image != self.next.image:
self._set_startup_image_file(self.next.image)
del_file_all(self.ops_conn, configured.image, slave)
except Exception as reason:
logging.error(reason)

# 4. reset stack member id


try:
self._reset_stack_member_id()
except Exception as reason:
logging.error(reason)

def set_startup_info(self, image_file, config_file, patch_file, memid_file, slave, esn_str):


"""Set the next startup information."""
logging.info("Set the next startup information...")
# 1. Set next startup system software
if image_file is not None:
try:
self._set_startup_image_file(image_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, image_file, slave)
self.reset_startup_info(slave)
raise

# 2. Set next startup config file


if config_file is not None:
try:
self._set_startup_config_file(config_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, config_file, slave)
self.reset_startup_info(slave)
raise

# 3. Set next startup patch file


if patch_file is not None:
try:
self._set_startup_patch_file(patch_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, patch_file, slave)
self.reset_startup_info(slave)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 45


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

raise

# 4. Set next member id


if memid_file is not None:
try:
self._set_stack_member_id(memid_file, esn_str)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, memid_file, None)
self.reset_startup_info(slave)
raise

def get_cwd(ops_conn):
"""Get the full filename of the current working directory"""
logging.info("Get the current working directory...")
uri = "/vfm/pwds/pwd"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<pwd>
<dictionaryName/>
</pwd>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the current working directory')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "dictionaryName", namespaces)
if elem is None:
raise OPIExecError('Failed to get the current working directory for no "directoryName" element')

return elem.text

def file_exist(ops_conn, file_path):


"""Returns True if file_path refers to an existing file, otherwise returns False"""
uri = "/vfm/dirs/dir"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<dir>
<fileName>$fileName</fileName>
</dir>
''')
req_data = str_temp.substitute(fileName = file_path)
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to list information about the file "%s"' % file_path)

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "fileName", namespaces)
if elem is None:
return False

return True

def del_file(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 46


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

</deleteFileUnRes>
''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete the file "%s" permanently' % file_path)

except Exception as reason:


logging.error(reason)

def del_file_noerror(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>
</deleteFileUnRes>
''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.info("file not exist")

except Exception as reason:


logging.error(reason)

def del_file_all(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file(ops_conn, file_path)
if slave:
del_file(ops_conn, 'slave#' + file_path)

def del_file_all_noerror(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file_noerror(ops_conn, file_path)
if slave:
del_file_noerror(ops_conn, 'slave#' + file_path)

def copy_file(ops_conn, src_path, dest_path):


"""Copy a file"""
print(('Info: Copy file %s to %s...' % (src_path, dest_path)))
sys.stdout.flush()
logging.info('Copy file %s to %s...', src_path, dest_path)
uri = "/vfm/copyFile"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<copyFile>
<srcFileName>$src</srcFileName>
<desFileName>$dest</desFileName>
</copyFile>
''')
req_data = str_temp.substitute(src = src_path, dest = dest_path)

# it is a action operation, so use create for HTTP POST


ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to copy "%s" to "%s"' % (src_path, dest_path))

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 47


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

def has_slave_mpu(ops_conn):
"""Whether device has slave MPU, returns a bool value"""
logging.info("Test whether device has slave MPU...")
uri = "/devm/phyEntitys"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<phyEntitys>
<phyEntity>
<entClass>mpuModule</entClass>
<entStandbyState/>
<position/>
</phyEntity>
</phyEntitys>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the device slave information')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
for entity in root_elem.findall(uri + 'phyEntity', namespaces):
elem = entity.find("vrp:entStandbyState", namespaces)
if elem is not None and elem.text == 'slave':
return True

return False

def get_system_info(ops_conn):
"""Get system info, returns a dict"""
logging.info("Get the system information...")
uri = "/system/systemInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<systemInfo>
<productName/>
<esn/>
<mac/>
</systemInfo>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the system information')

sys_info = {}.fromkeys(('productName', 'esn', 'mac'))


root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:')
nslen = len(namespaces['vrp'])
elem = root_elem.find(uri, namespaces)
if elem is not None:
for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}esn'
if tag in list(sys_info.keys()):
sys_info[tag] = child.text

return sys_info

def test_file_paths(image, config, patch, stack_memid, sha256_file, license_list_file):


"""Test whether argument paths are valid."""
logging.info("Test whether argument paths are valid...")
# check image file path
file_name = os.path.basename(image)
if file_name != '' and not file_name.lower().endswith('.cc'):
print('Error: Invalid filename extension of system software')
sys.stdout.flush()
return False

# check config file path

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 48


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

file_name = os.path.basename(config)
file_name = file_name.lower()
_, ext = os.path.splitext(file_name)
if file_name != '' and ext not in ['.cfg', '.zip', '.dat']:
print('Error: Invalid filename extension of configuration file')
sys.stdout.flush()
return False

# check patch file path


file_name = os.path.basename(patch)
if file_name != '' and not file_name.lower().endswith('.pat'):
print('Error: Invalid filename extension of patch file')
sys.stdout.flush()
return False

# check stack member id file path


file_name = os.path.basename(stack_memid)
if file_name != '' and not file_name.lower().endswith('.txt'):
print('Error: Invalid filename extension of stack member ID file')
sys.stdout.flush()
return False

# check sha256 file path


file_name = os.path.basename(sha256_file)
if file_name != '' and not file_name.lower().endswith('.txt'):
print('Error: Invalid filename extension of sha256 file')
sys.stdout.flush()
return False

# check license list file path


file_name = os.path.basename(license_list_file)
if file_name != '' and not file_name.lower().endswith('.xml'):
print('Error: Invalid filename extension of license list file')
sys.stdout.flush()
return False

return True

def sha256sum(fname, need_skip_first_line = False):


"""
Calculate sha256 num for this file.
"""

def read_chunks(fhdl):
'''read chunks'''
chunk = fhdl.read(8096)
while chunk:
yield chunk
chunk = fhdl.read(8096)
else:
fhdl.seek(0)

sha256_obj = hashlib.sha256()
if isinstance(fname, str) and os.path.exists(fname):
with open(fname, "rb") as fhdl:
#skip the first line
fhdl.seek(0)
if need_skip_first_line:
fhdl.readline()
for chunk in read_chunks(fhdl):
sha256_obj.update(chunk)
elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file):
for chunk in read_chunks(fname):
sha256_obj.update(chunk)
else:
pass
return sha256_obj.hexdigest()

def sha256_get_from_file(fname):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 49


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

"""Get sha256 num form file, stored in first line"""

with open(fname, "r") as fhdl:


fhdl.seek(0)
line_first = fhdl.readline()

# if not match pattern, the format of this file is not supported


if not re.match('^#sha256sum="[\\w]{64}"[\r\n]+$', line_first):
return 'None'

return line_first[12:76]

def sha256_check_with_first_line(fname):
"""Validate sha256 for this file"""

fname = os.path.basename(fname)
sha256_calc = sha256sum(fname, True)
sha256_file = sha256_get_from_file(fname)

if sha256_file.lower() != sha256_calc:
logging.warning('sha256 check failed, file %s', fname)
print(('sha256 checksum of the file "%s" is %s' % (fname, sha256_calc)))
sys.stdout.flush()
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256_calc)
print(('sha256 checksum received from the file "%s" is %s' % (fname, sha256_file)))
sys.stdout.flush()
logging.warning('sha256 checksum received from the file "%s" is %s', fname, sha256_file)
return False

return True

def sha256_check_with_dic(sha256_dic, fname):


"""sha256 check with dic"""
if fname not in sha256_dic:
logging.info('sha256_dic does not has key %s, no need to do sha256 verification', fname)
return True

sha256sum_result = sha256sum(fname, False)


if sha256_dic[fname] == sha256sum_result:
return True

print(('sha256 checksum of the file "%s" is %s' % (fname, sha256sum_result)))


sys.stdout.flush()
print(('sha256 checksum received for the file "%s" is %s' % (fname, sha256_dic[fname])))
sys.stdout.flush()
logging.warning('sha256 check failed, file %s', fname)
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256sum_result)
logging.warning('sha256 checksum received for the file "%s" is %s', fname, sha256_dic[fname])

return False

def parse_sha256_file(fname):
"""parse sha256 file"""

def read_line(fhdl):
"""read a line by loop"""
line = fhdl.readline()
while line:
yield line
line = fhdl.readline()
else:
fhdl.seek(0)

sha256_dic = {}
with open(fname, "rb") as fhdl:
for line in read_line(fhdl):
line_spilt = line.split()
if 2 != len(line_spilt):
continue

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 50


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

dic_tmp = {line_spilt[0]: line_spilt[1]}


sha256_dic.update(dic_tmp)
return sha256_dic

def verify_and_parse_sha256_file(fname):
"""
vefiry data integrity of sha256 file and parse this file

format of this file is like:


------------------------------------------------------------------
#sha256sum="517cf194e2e1960429c6aedc0e4dba37"

file-name sha256
conf_5618642831132.cfg c0ace0f0542950beaacb39cd1c3b5716
------------------------------------------------------------------
"""
if not sha256_check_with_first_line(fname):
return ERR, None
return OK, parse_sha256_file(fname)

def check_parameter(aset):
seq = ['&', '>', '<', '"', "'"]
if aset:
for c in seq:
if c in aset:
return True
return False

def check_filename(ops_conn):
sys_info = get_system_info(ops_conn)
url_tuple = urlparse(FILE_SERVER)
if check_parameter(url_tuple.username) or check_parameter(url_tuple.password):
raise ZTPErr('Invalid username or password, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of system software, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_CONFIG)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of configuration file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_PATCH.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of patch file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_MEMID)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of member ID file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_SHA256)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of sha256 file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_LICLIST)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of license list file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
return OK

def active_license(ops_conn, license_name):


if license_name:
uri = "/lcs/lcsActive"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<lcsActive>
<lcsFileName>$lcsFileName</lcsFileName>
</lcsActive>
''')
req_data = str_temp.substitute(lcsFileName = license_name)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.error('Error: Failed to active license.')
return ERR
return OK

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 51


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

def main_proc(ops_conn):
"""Main processing"""
sys_info = get_system_info(ops_conn) # Get system info, such as esn and system mac
cwd = get_cwd(ops_conn) # Get the current working directory
startup = Startup(ops_conn)
slave = has_slave_mpu(ops_conn) # Check whether slave MPU board exists or not
chg_flag = False

check_filename(ops_conn)

# check remote file paths


if not test_file_paths(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''), REMOTE_PATH_CONFIG,
REMOTE_PATH_PATCH.get(sys_info['productName'], ''), REMOTE_PATH_MEMID,
REMOTE_PATH_SHA256,
REMOTE_PATH_LICLIST):
return ERR

# download sha256 file first, used to verify data integrity of files which will be downloaded next
local_path_sha256 = None
file_path = REMOTE_PATH_SHA256
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_sha256 = cwd + file_name
ret = download_file(ops_conn, url, local_path_sha256, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download sha256 file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download sha256 file successfully')
sys.stdout.flush()
ret, sha256_dic = verify_and_parse_sha256_file(file_name)
# delete the file immediately
del_file_all(ops_conn, local_path_sha256, None)
if ret is ERR:
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
return ERR
else:
sha256_dic = {}

# download configuration file


local_path_config = None
file_path = REMOTE_PATH_CONFIG
if "%s" in file_path:
file_path = REMOTE_PATH_CONFIG % sys_info['esn']
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_config = cwd + file_name
del_file_noerror(ops_conn, local_path_config)
ret = download_file(ops_conn, url, local_path_config, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download configuration file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download configuration file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_config)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 52


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

copy_file(ops_conn, local_path_config, 'slave#' + local_path_config)


chg_flag = True

# download patch file


local_path_patch = None
file_path = REMOTE_PATH_PATCH.get(sys_info['productName'], '')
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if startup.current.patch:
cur_pat = os.path.basename(startup.current.patch).lower()
else:
cur_pat = ''
if file_name != '' and file_name.lower() != cur_pat:
url = FILE_SERVER + file_path
local_path_patch = cwd + file_name
del_file_noerror(ops_conn, local_path_patch)
ret = download_file(ops_conn, url, local_path_patch, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download patch file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
print('Info: Download patch file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_patch)
copy_file(ops_conn, local_path_patch, 'slave#' + local_path_patch)
chg_flag = True

# download stack member ID file


local_path_memid = None
file_path = REMOTE_PATH_MEMID
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_memid = cwd + file_name
del_file_noerror(ops_conn,local_path_memid)
ret = download_file(ops_conn, url, local_path_memid, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
return ERR
print('Info: Download stack member ID file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
chg_flag = True
#no need copy to slave board

# download system software


local_path_image = None
file_path = REMOTE_PATH_IMAGE.get(sys_info['productName'], '')
if not file_path.startswith('/'):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 53


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

file_path = '/' + file_path


file_name = os.path.basename(file_path)
if startup.current.image:
cur_image = os.path.basename(startup.current.image).lower()
else:
cur_image = ''
if file_name != '' and file_name.lower() != cur_image:
url = FILE_SERVER + file_path
local_path_image = cwd + file_name
del_file_noerror(ops_conn, local_path_image)
ret = download_file(ops_conn, url, local_path_image, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
if file_exist(ops_conn, file_name):
del_file_all(ops_conn, local_path_image, slave)
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
print('Info: Download system software file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_image)
copy_file(ops_conn, local_path_image, 'slave#' + local_path_image)
chg_flag = True
# download license list file
local_path_liclist = None
file_path = REMOTE_PATH_LICLIST
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
download_space = os.path.dirname(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_liclist = cwd + file_name
del_file_noerror(ops_conn, local_path_liclist)
ret = download_file(ops_conn, url, local_path_liclist, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license list file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download license list file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
chg_flag = True

#execute license list file to get license file name which end with .dat
license_name = None
if local_path_liclist is not None:
tree = etree.parse(file_name)
root = tree.getroot()
for child in root.findall('Lic'):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 54


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

name = child.get('name')
esn = child.find('Esn').text
if sys_info['esn'] in esn:
license_name = name
print(('Info: License file name is "%s"' % license_name))
sys.stdout.flush()
break
if license_name == None :
print('Warning: Esn of this device is not in the license list file')
sys.stdout.flush()
#del_file_all(ops_conn, local_path_config, slave)
#del_file_all(ops_conn, local_path_patch, slave)
#del_file_all(ops_conn, local_path_memid, slave)
#del_file_all(ops_conn, local_path_image, slave)
#del_file_all(ops_conn, local_path_liclist, slave)
#return ERR

# download license file


local_path_license = None
file_path = license_name
if file_path is not None:
if not file_path.startswith('/'):
file_path = '/' + file_path
file_path = download_space + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_license = cwd + file_name
ret = download_file(ops_conn, url, local_path_license, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
print('Info: Download license file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR
chg_flag = True
#no need copy to slave board

# download python file


local_path_python = None
file_path = REMOTE_PATH_PYTHON
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_python = cwd + file_name
del_file_noerror(ops_conn, local_path_python)
ret = download_file(ops_conn, url, local_path_python, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download python file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 55


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

del_file_all(ops_conn, local_path_memid, slave)


del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR
print('Info: Download python file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_python)
copy_file(ops_conn, local_path_python, 'slave#' + local_path_python)
chg_flag = True

if chg_flag is False:
return ERR

# active license file


if local_path_license is not None:
ret = active_license(ops_conn, local_path_license)
if ret is ERR:
print('Info: Active license failed')
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
print(('Info: Active license sucessfully, name: %s' % local_path_license))
sys.stdout.flush()
sleep(10)
# set startup info
startup.set_startup_info(local_path_image, local_path_config, local_path_patch,
local_path_memid, slave, sys_info['esn'])

# delete stack member ID file and license list file after used
del_file_all(ops_conn, local_path_memid, None)
del_file_all(ops_conn, local_path_liclist, None)

return OK

def main(usb_path = ''):


"""The main function of user script. It is called by ZTP frame, so do not remove or change this function.

Args:
Raises:
Returns: user script processing result
"""
host = "localhost"
if usb_path and len(usb_path):
logging.info('ztp_script usb_path: %s', usb_path)
global FILE_SERVER
FILE_SERVER = 'file:///' + usb_path
try:
# Make an OPS connection instance.
ops_conn = OPSConnection(host)
ret = main_proc(ops_conn)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 56


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

except OPIExecError as reason:


logging.error('OPI execute error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except ZTPErr as reason:


logging.error('ZTP error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except IOError as reason:


print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except Exception as reason:


logging.error(reason)
traceinfo = traceback.format_exc()
logging.debug(traceinfo)
ret = ERR

finally:
# Close the OPS connection
ops_conn.close()

return ret

if __name__ == "__main__":
main()

Python Script Description


NOTE

Information in bold can be modified based on the actual running environment.


● Specifies the SHA256 code of the script file.
#sha256sum="7eb57f7cf11d2bf481e544f333267e8dd43a532c12a60151d640d64872013a8b"
You can use the SHA256 code to check the integrity of the script file
downloaded by the device.
You can use an SHA256 calculation tool, such as sha256sum, to generate the
SHA256 code of the script file.
NOTE

The script file cannot contain #sha256sum= when the SHA256 code is generated. Add
#sha256sum= to the beginning of the script file after the SHA256 code is generated.
When the SHA256 code of a script file is empty or incorrect, this script file can still be
executed.
● Specifies the file obtaining mode.
FILE_SERVER = 'sftp://sftpuser:Pwd123@10.1.3.2'
You can obtain version files from a TFTP, FTP, or SFTP server. Based on the
server used, the path can be any of the following:
– tftp://hostname
– ftp://[username[:password]@]hostname[:port]
– sftp://[username[:password]@]hostname[:port]
The parameters username, password, and port are optional.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 57


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

If the device uses a USB flash drive for automatic deployment, set this field to ''.
TFTP and FTP pose security risks, and SFTP is recommended for file transfer.
● Specify the path and file name of the system software.
REMOTE_PATH_IMAGE = {
'CE12800' : '/image/CE6800-V200R019C10.cc',}
If the device does not need to load the system software, set this field to
empty, for example:
REMOTE_PATH_IMAGE = {
'CE12800' : '',
}
● Specifies the path and name of the configuration file.
REMOTE_PATH_CONFIG = '/config/conf_%s.cfg'
%s indicates a device serial number or MAC address that is used to obtain a
configuration file.
– If %s indicates a device serial number, the following part in the def
main_proc() function must be set to esn.
# download configuration file
local_path_config = None
file_path = REMOTE_PATH_CONFIG % sys_info['esn']
– If %s indicates a device MAC address, the following part in the def
main_proc() function must be set to mac.
# download configuration file
local_path_config = None
file_path = REMOTE_PATH_CONFIG % sys_info['mac']

NOTE

● The serial number or MAC address in the file name must be uppercase.
● If %s indicates a device MAC address, the MAC address in the file name must be the
device system MAC address plus 1. For example, if the device system MAC address is
E468-A356-0DD0, the file name must be conf_E468-A356-0DD1.cfg.
● Specifies the path and name of the patch file. (The following example is a
patch file name. Use the latest patch file.)
REMOTE_PATH_PATCH = {
'CE12800' : '/patch/CE12800-V200R001SPH001.PAT',}
The path and file name format of the patch file are the same as those of the
system software.
● Specifies the path and name of the stack member ID file.
REMOTE_PATH_MEMID = '/stack/stack_member.txt'
If multiple unconfigured devices automatically set up a stack after they
complete automatic deployment, configure a stack member ID for each
device.
See 2.2.5 Automatic Stacking in ZTP for the format of the stack member ID
file.
This parameter is mandatory if a stack must be set up; otherwise, set this
parameter to ''.
● Specifies the path and name of the license file.
REMOTE_PATH_LICLIST = 'Index.xml'
You can use a license list file to enable switches to automatically install
licenses.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 58


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

2.2.7 Installing Licenses in a Batch shows the license list file format.
If the switch does not need to load a license, set this field to ".
● Specifies the path and name of the SHA256 verification file.
REMOTE_PATH_SHA256 = '/sha256.txt'

You can use the SHA256 verification file to check the integrity of the files
downloaded by the device.
See 2.2.6 Checking the Integrity of Version Files for the format of the
SHA256 verification file.
If the downloaded files do not need to be checked, set this field to ''.
● Specifies the path and name of the user-defined file to be downloaded.
REMOTE_PATH_PYTHON = '/get_systeminfo.py'

This field specifies the path and name of the user-defined file to be
downloaded. If the path and name of the user-defined file does not need to
be specified, set this field to ''.
● Defines the interval for collecting device startup information.
GET_STARTUP_INTERVAL = 15

This field defines the interval for collecting device startup information.
● Defines the maximum number of attempts to obtain device startup
information.
MAX_TIMES_GET_STARTUP = 120

This field defines the maximum number of attempts to obtain device startup
information.
● Defines the number of retries to download files.
MAX_TIMES_RETRY_DOWNLOAD = 3

This field defines the number of retries to download files.


● Creates an OPS connection.
class OPSConnection()

You do not need to edit this field.


● Defines the HTTP header.
def __init__()

You do not need to edit this field.


● Encapsulates the OPS connection.
self.conn = httplib.HTTPConnection()

You do not need to edit this field.


● Closes the OPS connection.
def close()

You do not need to edit this field.


● Defines an add action.
def create()

You do not need to edit this field.


● Defines a delete action.
def delete()

You do not need to edit this field.


● Defines an obtain action.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 59


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

def get()

You do not need to edit this field.


● Defines a configure action.
def set()

You do not need to edit this field.


● Sets the format of request code to REST.
def _rest_call()

You do not need to edit this field.


● Displays debugging logs.
logging.debug()

You do not need to edit this field.


● Indicates an OPS process error.
class OPIExecError()

You do not need to edit this field.


● Indicates an automatic deployment error.
class ZTPErr()

You do not need to edit this field.


● Resolves domain names.
def get_addr_by_hostname()

You do not need to edit this field.


● Clears the RSA key after SFTP download fails.
def _del_rsa_peer_key()

You do not need to edit this field.


● Clears the IP address of the SSH server and RSA key after SSH download fails.
def _del_sshc_rsa_key()

You do not need to edit this field.


● Configures user attributes for SSH client authentication at the first login.
def _set_sshc_first_time()

You do not need to edit this field.


● Downloads files in SFTP mode.
def _sftp_download_file()

You do not need to edit this field.


● Downloads files from a USB flash drive.
def _usb_download_file()

You do not need to edit this field.


● Defines file download parameters.
def download_file()

You do not need to edit this field.


● Obtains startup information and starts the device.
class StartupInfo()
class Startup()

You do not need to edit this field.


● Obtains the working directory of the user.
def get_cwd()

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 60


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

You do not need to edit this field.


● Checks whether the file to be downloaded exists.
def file_exist()

You do not need to edit this field.


● Deletes files after a load failure.
def del_file()

If files fail to be loaded, they are deleted so that the device can revert to the
state it was in before ZTP started. This facilitates subsequent operations.
You do not need to edit this field.
● Copies a file.
def copy_file()

You do not need to edit this field.


● Checks whether the device has a standby MPU.
def has_slave_mpu()

You do not need to edit this field.


● Obtains the system information.
def get_system_info()

You do not need to edit this field.


● Checks whether the file path is available.
def test_file_paths()

You do not need to edit this field.


● Verifies the file using the SHA256 code.
def sha256sum()
def sha256_get_from_file()
def sha256_check_with_first_line()
def sha256_check_with_dic()
def parse_sha256_file()
def verify_and_parse_sha256_file()

You do not need to edit this field.


● Checks whether the user name, password, and file name contain special
characters.
def check_parameter()
def check_filename()

You do not need to edit this field.


● Activates the license.
def active_license()

You do not need to edit this field.


● Defines the overall automatic deployment process.
def main_proc()
def main()
if __name__ == "__main__":
main()

You do not need to edit this field.


The main function must be provided; otherwise, the script cannot be
executed.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 61


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

2.2.4 Intermediate File in Python Format (Connection to the


iMaster NCE-Fabric)

For details about how to deploy the ZTP function on a device connected to the
iMaster NCE-Fabric, see ZTP for Automated Development of the Underlay
Network in the Huawei CloudFabric Solution Product Documentation.

2.2.5 Automatic Stacking in ZTP

To enable multiple unconfigured devices to set up a stack after automatic


deployment is complete, configure a stack member ID for each member device
using the stack member ID file.

The stack member ID file must be a text file, with the name extension .txt. The
format is as follows:
ESN Stack group Stack member
21023553380DCCE12823 10 1
21023553380DCCE12824 10 2

Table 2-2 Description of fields in the stack member ID file

Field Mandatory or Description


Optional

ESN Mandatory Specifies the equipment serial


number (ESN) of a device.

Stack group Mandatory Indicates the stack ID used to


identify different stacks.

Stack member Mandatory Specifies the stack member ID of


the device.

2.2.6 Checking the Integrity of Version Files

You can use the SHA256 verification file to check the integrity of the files
downloaded by the device. The SHA256 code of a downloaded file is saved in the
SHA256 verification file in advance. After downloading the file, the device
generates an SHA256 code for the file and compares it with that in the SHA256
verification file. If the two SHA256 codes are different, the downloaded file fails
the integrity check and will not be loaded by the device.

The file name extension of the SHA256 verification file must be .txt. The file
format is as follows:
#sha256sum="517cf194e2e1960429c6aedc0e4dba37"
file-name sha256
conf_5618642831132.cfg c0ace0f0542950beaacb39cd1c3b5716

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 62


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Table 2-3 Description of fields in the SHA256 verification file


Field Mandatory or Description
Optional

#sha256sum Mandatory Specifies the SHA256 code of the


SHA256 verification file.

file-name Mandatory Specifies a file name.

sha256 Mandatory Specifies the SHA256 code


generated by the device.

2.2.7 Installing Licenses in a Batch


You can use a license list file to enable switches to install licenses in a batch.
Information about the licenses to be installed on switches is saved in the license
list file in advance. After a switch downloads the license list file, it downloads and
installs the required license file based on its ESN.
The license list file must be in XML format and use the file name extension .xml.
The format is as follows:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Index formatVersion="1.0">
<Lic name="CloudEngine8800V200R002_20171110F6TE50.xml">
<LSN>LIC20171110F6TE50</LSN>
<Esn>2102350KGU10G6000025</Esn>
</Lic>
<Lic name="CloudEngine8800V200R002_20171110F6TH50.xml">
<LSN>LIC20171110F6TH50</LSN>
<Esn>2102350KGU10G6000026</Esn>
</Lic>
</Index>

NOTE

● You can directly use the Index.xml file in the .zip package obtained during license
download as the license list file. If there are other list files in the .zip package, for
example, Index1.xml, ignore the file. You can also edit the license list file according to
the format requirements. The list file name can be customized.
● If licenses do not need to be downloaded in a batch, you need to edit the Index.xml file
as the license list file according to format requirements.

Table 2-4 Fields in the license list file


Field Mandatory or Description
Optional

Lic Mandatory License file information. The name


attribute indicates the license file
name.

LSN Optional File generation serial number.

Esn Mandatory ESN of a switch.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 63


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

2.3 Licensing Requirements and Limitations for ZTP


This document is applicable to non-solution scenarios. If the ZTP function is
required in a solution scenario, see ZTP for Automated Deployment of the
Underlay Network in the Huawei CloudFabric Solution Product
Documentation.

Involved Network Elements


USB-based automatic deployment requires:
● USB flash drive
DHCP-based automatic deployment requires:
● DHCP server: assigns a temporary management IP address, default gateway,
DNS server address, and intermediate file server address to the device running
ZTP.
● Syslog server: uploads user logs recorded during the ZTP process to the NMS.
● DHCP relay agent: relays packets exchanged between the device running ZTP
and DHCP server located on different network segments.
● Intermediate file server: stores the intermediate file required by the device
running ZTP. By parsing the intermediate file, the device running ZTP obtains
information about the version files and version file server address.
● Version file server: stores the version files to be loaded to unconfigured
devices, including the system software, configuration files, and patch files. The
version file server and intermediate file server can be deployed on the same
server.
● DNS server: provides mappings between domain names and IP addresses,
and resolves the file server domain name to an IP address for the ZTP-running
device. Based on the resolved IP address, the device can obtain requested files
from the file server.

Licensing Requirements
ZTP is a basic function of the switch, and as such is controlled by the license for
basic software functions. The license for basic software functions has been loaded
and activated before delivery. You do not need to manually activate it.

Version Requirements

Table 2-5 Products and minimum version supporting ZTP


Product Minimum Version
Required

CE12804, CE12808, CE12812, and CE12816 V100R003C00

CE12804S and CE12808S V100R005C00

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 64


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

For details about the mapping between software versions and switch models, see the
Hardware Query Tool.
Software version evolution: V100R001C00 -> V100R002C00 -> V100R003C00 ->
V100R003C10 -> V100R005C00 -> V100R005C10 -> V100R006C00 -> V200R001C00 ->
V200R002C50 -> V200R003C00 -> V200R005C00 -> V200R005C10 -> V200R019C00 ->
V200R019C10 -> V200R020C10 -> V200R021C00

Feature Limitations
● If two MPUs are installed, you can only log in and check ZTP information
through the console port of the active MPU. If you log in to the standby MPU,
the system displays a message indicating that the other MPU is executing the
ZTP process. Before starting the ZTP process, you need to connect the two
MPUs to their respective console ports.
● If unconfigured devices need to set up a stack after they complete automatic
deployment, use a Python script as an intermediate file. An INI file cannot be
used for stack setup.
● If you want to implement automatic device deployment and stack setup, the
configuration file loaded to the device must contain the stack configuration
related to this stack member ID, including the stack priority, stack domain ID,
stack connection mode, and stack port.
● If the device implements automatic deployment using a USB flash drive, the
intermediate file name must be ztp_config.ini or ztp_script.py. If the device
implements automatic deployment using DHCP, the intermediate file name
can be user-defined.
● The configured user name, password, and version file name in the
intermediate file cannot contain special characters, including & > < " ' / #.
● If the device to be powered on has the cfg file with the same name as the
new cfg file, the new cfg file cannot be downloaded to this device. You are
advised to use ESNs of devices to differentiate cfg files, for example,
conf_5618642831132.cfg.
● A Python script file must be in Windows or Unix format.
● Versions earlier than V200R019C10 support Python 2.0 script files, and
V200R019C10 and later versions support Python 3.0 script files.
● ZTP needs to be deployed on a private network because using a non-private
network to deploy ZTP has security risks.
● TFTP and FTP have security risks. You are advised to use SFTP for ZTP.
● Currently, the Syslog server can only use an IPv4 address.
● ZTP supports only Admin-VS.
● Currently, the Syslog server address can be obtained only through the Option
field configured on the DHCP server rather than through the intermediate file.
● If the available space on the device is insufficient, you need to manually
delete unnecessary system software packages. Otherwise, ZTP will fail.

2.4 Default Settings for ZTP

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 65


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Table 2-6 Default settings for ZTP

Item Default Setting

ZTP function Enabled

2.5 Configuring an Unconfigured Device to Implement


Automatic Deployment Using a USB Flash Drive
Pre-configuration Tasks
Before configuring ZTP, complete the following tasks:
● Ensure that the unconfigured device has no configuration file.

Configuration Procedure
The following configuration tasks are mandatory and must be performed in
sequence.

2.5.1 Enabling the ZTP Function

Background
To enable an unconfigured device to automatically start the ZTP process, the ZTP
function must be enabled on the device. The ZTP function is enabled on devices by
default.

Procedure
Step 1 (Optional) Run the display system ztp command to check whether the device
starts the ZTP process at the next startup without configuration.

Step 2 Run the set ztp enable command to enable the ZTP function.

By default, the ZTP function is enabled on devices.

To disable an unconfigured device from starting the ZTP process, run the set ztp
disable command on the device.

----End

2.5.2 Editing the Intermediate File

Background
An intermediate file can be an INI file or a Python script. You can choose the file
type as required. For details about the file format, see 2.2.2 Intermediate File in
INI Format and 2.2.3 Intermediate File in Python Format.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 66


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

● If an .ini file is used as the intermediate file, the file name must be ztp_config.ini. If a
Python script is used as the intermediate file, the file name must be ztp_script.py.
● If unconfigured devices need to set up a stack after they complete automatic deployment,
use a Python script as an intermediate file. An INI file cannot be used for stack setup.

Procedure
Step 1 Edit the intermediate file according to the file type and format.
Step 2 (Optional) To set up a stack or verify the downloaded file, edit the stack member
ID file and SHA256 verification file. For details, see 2.2.5 Automatic Stacking in
ZTP and 2.2.6 Checking the Integrity of Version Files.
Step 3 Save the intermediate file and version files. If the stack member ID file or SHA256
check file exists, you also need to save the file to the USB flash drive.
The intermediate file must be saved to the root directory of the USB flash drive.
The stack member ID file, SHA256 code file, and version files are saved to the
directory specified in the intermediate file.

----End

2.5.3 Powering on the Device

Context
After saving the required files to the USB flash drive, connect it to an unconfigured
device and power on the device. The device then automatically downloads version
files and restarts to complete automatic deployment.

Procedure
Step 1 Connect the USB flash drive to the unconfigured device.
Step 2 Power on the device.

----End

2.5.4 Verifying the Configuration

Procedure
Step 1 The devices complete the ZTP process in about 15 minutes after they are powered
on. Then you can log in to the device and run the display startup command to
check whether the startup files are the required ones.
Step 2 If ZTP fails, analyze ZTP logs on the device to determine the causes.
ZTP logs are saved in the file named ztp_YYYYMMHHMMSS.log in the flash:/
directory.

----End

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 67


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

2.6 Configuring an Unconfigured Device to Implement


Automatic Deployment Through DHCP
Pre-configuration Tasks
Before configuring ZTP, complete the following tasks:
● Ensure that there are reachable routes between the DHCP server, file server,
and unconfigured device.
● Ensure that the unconfigured device has no startup configuration file.

Configuration Procedure
In the following configuration tasks, 2.6.2 Editing an Intermediate File, 2.6.3
Configuring a DHCP Server and DHCP Relay Agent, and 2.6.4 Configuring a
File Server are mandatory and can be performed in any sequence. After
completing the preceding configuration tasks, power on the device to start the
ZTP process.

2.6.1 Enabling the ZTP Function

Background
To enable an unconfigured device to automatically start the ZTP process, the ZTP
function must be enabled on the device. The ZTP function is enabled on devices by
default.

Procedure
Step 1 (Optional) Run the display system ztp command to check whether the device
starts the ZTP process at the next startup without configuration.

Step 2 Run the set ztp enable command to enable the ZTP function.

By default, the ZTP function is enabled on devices.

To disable an unconfigured device from starting the ZTP process, run the set ztp
disable command on the device.

----End

2.6.2 Editing an Intermediate File

Context
An intermediate file can be an INI file or a Python script. You can choose the file
type as required. For details about the file format, see 2.2.2 Intermediate File in
INI Format and 2.2.3 Intermediate File in Python Format.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 68


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

If unconfigured devices need to set up a stack after they complete automatic deployment, use a
Python script as an intermediate file. An INI file cannot be used for stack setup.

Procedure
Step 1 Edit the intermediate file according to the file type and format.

Step 2 (Optional) To set up a stack or verify the downloaded file, edit the stack member
ID file and SHA256 verification file. For details, see 2.2.5 Automatic Stacking in
ZTP and 2.2.6 Checking the Integrity of Version Files.

----End

2.6.3 Configuring a DHCP Server and DHCP Relay Agent

Context
Before powering on an unconfigured device, deploy a DHCP server from which the
unconfigured device can obtain the IP address, gateway address, intermediate file
server address, and intermediate file name.

The device sends to the DHCP server a DHCP Discover packet that carries DHCP
Option 60 and Option 61. DHCP Option 60 (Vendor class identifier) records the
device manufacturer and model, and DHCP Option 61 (Client identifier) records
the device ESN and MAC address.

Table 2-7 describes the options that need to be configured on the DHCP server.

CAUTION

● The DHCP server does not support authentication and may be spoofed. You are
advised to use a trusted DHCP server for deployment on a secure network.
● DHCP is a non-encrypted transmission protocol. The user name and password
of the file server carried in the Option 66 and Option 67 fields have security
risks. You are advised to use this protocol on a secure network.

Table 2-7 Description of options

Option No. Mandatory Function


or Optional

1 Mandatory Specifies the subnet mask of the IP address.

3 Mandatory Configures the egress gateway of the DHCP


client.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 69


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Option No. Mandatory Function


or Optional

6 Optional Specifies the IP address of the DNS server. If you


configure the domain name of the intermediate
file server (for example, www.ztp.com) as its
host name, deploy a DNS server to resolve the
domain name to the corresponding IP address. If
you configure the IP address of the intermediate
file server as its host name, you do not need to
deploy a DNS server.

7 Optional Specifies the IP address of the Syslog server.

66 Optional Specifies the host name of the intermediate file


server. An intermediate file server can be a TFTP,
FTP, or SFTP server. The format of this field is as
follows:
● tftp://hostname
● ftp://[username[:password]@]hostname
● sftp://
[username[:password]@]hostname[:port]
The value of hostname can be a domain name
or an IP address. If hostname is set to a domain
name, a DNS server is required.
NOTE
The configured user name and password cannot
contain special characters: forward slash (/) and
number sign (#).
TFTP and FTP pose security risks, and SFTP is
recommended for file transfer.

67 Mandatory Specifies the name of the intermediate file. The


intermediate file name can be *.ini or ,*.py, and
has a maximum length of 255 bytes.
The intermediate file name is in the format
path/filename. In the format, path can contain
or does not contain the host name of the file
server. For example, the file name can be /
script/ztp_script.py without a host name or
http://10.13.78.24:8080/script/ztp_script.py
with a host name. If the path without a host
name is used, you must configure Option 66 on
the DHCP server.

150 Optional Specifies the IP address of the TFTP server. Only


one TFTP server IP address is resolved.

NOTE

The lease of an IP address applied by a DHCP client is at least 1 hour.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 70


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

In following procedures, CloudEngine switches function as the DHCP server or


DHCP relay agent. If a device of a different type serves as the DHCP server or
relay agent, see the documentation of the corresponding product for configuration
details.

Procedure
Step 1 Perform the following steps on the DHCP server:
1. Run the system-view command to enter the system view.
2. Run the dhcp enable command to enable DHCP.
3. Run the dhcp server request-packet all-interface enable command to
enable the DHCP server function on all interfaces. By default, the DHCP server
function is disabled on all interfaces.
4. Run the ip pool ip-pool-name command to create a global address pool and
enter its view.
5. Run the gateway-list ip-address &<1-8> command to set an egress gateway
address for DHCP clients.
6. Run the network ip-address [ mask { mask | mask-length } ] command to
specify the range of IP addresses to be allocated to DHCP clients.
7. Run the option code [ sub-option sub-code ] { ascii ascii-string | hex hex-
string | cipher cipher-string | ip-address ip-address &<1-8> } command to
configure DHCP Option 66, Option 67, or Option 150. See Table 2-7 for DHCP
options to be configured.
NOTE

When the password is contained in the option, the ascii or hex type is insecure. Set the
option type to cipher. A secure password should contain at least two types of the
following: lowercase letters, uppercase letters, digits, and special characters. In addition,
the password must consist of at least six characters.
8. Run the commit command to commit the configuration.
Step 2 (Optional) Perform the following steps on the DHCP relay agent:
If the unconfigured device and DHCP server are on different network segments,
configure a DHCP relay agent to forward DHCP packets exchanged between them.
1. Run the system-view command to enter the system view.
2. Run the dhcp enable command to enable DHCP.
3. Run the interface interface-type interface-number command to enter the
interface view.
4. On an Ethernet interface, run undo portswitch
The interface is switched to Layer 3 mode.
By default, an Ethernet interface works in Layer 2 mode.
The mode switching function takes effect when the interface only has
attribute configurations (for example, shutdown and description
configurations). Alternatively, if configuration information supported by both
Layer 2 and Layer 3 interfaces exists (for example, mode lacp and lacp
system-id configurations), no configuration that is not supported after the
working mode of the interface is switched can exist. If unsupported

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 71


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

configurations exist on the interface, delete the configurations first and then
run the undo portswitch command.

NOTE

If many Ethernet interfaces need to be switched to Layer 3 mode, run the undo
portswitch batch interface-type { interface-number1 [ to interface-number2 ] }
&<1-10> command in the system view to switch these interfaces to Layer 3 mode in
batches.
5. Run the ip address ip-address { mask | mask-length } command to configure
an IP address for the interface.
6. Run the dhcp select relay command to enable the DHCP relay function.
7. Run the dhcp relay binding server ip ip-address command to specify the
DHCP server IP address on the DHCP relay agent.
8. Run the commit command to commit the configuration.

----End

2.6.4 Configuring a File Server

Context
A file server stores the files to be downloaded to unconfigured devices, including
intermediate files and version files. You can use a switch as a file server. A file
server must have sufficient space to store files. Before configuring a switch as a
file server, ensure that its storage space is sufficient. To ensure the device
performance, a third-party file server is typically used on a ZTP network. For
details about how to configure a third-party file server, see the third-party server
operation guide.

The intermediate file server and version file server can be the same file server. The
file server can be a TFTP, FTP, or SFTP server.

NOTE

There must be reachable routes between the file server and unconfigured device.

Follow-up Procedure
After the file server is configured, save the intermediate file and version files to
the file server. If the stack member ID file or SHA256 verification file exists, you
also need to save the file to the file server.

NOTE

To ensure security of the file server, configure a unique user name for the file server and
assign the read-only permission to the user to prevent unauthorized modification of the
files. After the ZTP process is complete, disable the file server function.

2.6.5 Powering on the Device

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 72


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Context
After completing the preceding configuration, power on the unconfigured device.
The device then automatically downloads version files and restarts to complete
automatic deployment.

Procedure
Step 1 Power on the device.

----End

2.6.6 Verifying the Configuration

Procedure
Step 1 The devices complete the ZTP process in about 15 minutes after they are powered
on. Then you can log in to the device and run the display startup command to
check whether the startup files are the required ones.
Step 2 Run the display system ztp command to check whether the device completes
automatic deployment through ZTP.
Step 3 If ZTP fails, analyze ZTP logs on the device to determine the causes.
ZTP logs are saved in the file named ztp_YYYYMMHHMMSS.log in the flash:/
directory.

----End

2.7 Configuring a DHCP Client

2.7.1 Enabling the ZTP Function


Background
To enable an unconfigured device to automatically start the ZTP process, the ZTP
function must be enabled on the device. ZTP is enabled on devices by default.
NOTE

Connect the management interface of the unconfigured device to the DHCP server or relay
agent.

Procedure
Step 1 (Optional) Run the display system ztp command to check whether the device
starts the ZTP process at the next startup without configuration.
Step 2 Run the set ztp enable command to enable the ZTP function.
By default, the ZTP function is enabled on devices.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 73


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

To disable an unconfigured device from starting the ZTP process upon the startup,
run the set ztp disable command on the device.

----End

2.7.2 Configuring a DHCP Server and DHCP Relay Agent

Context
The device functions as a DHCP client and periodically broadcasts DHCP request
packets through the management interface to obtain configuration information.
The DHCP server uses option fields to transmit network configuration parameters
to the client to implement automatic configuration for ZTP deployment. Table 2-8
describes the options that need to be configured on the DHCP server.

CAUTION

● The DHCP server does not support authentication and may be spoofed. You are
advised to use a trusted DHCP server for deployment on a secure network.
● DHCP is a non-encrypted transmission protocol. The user name and password
for SSH login carried in the Option 66 field have security risks. You are advised
to use this protocol on a secure network.

Table 2-8 Description of options

Option No. Mandatory Function


or Optional

1 Mandatory Specifies the subnet mask of the IP address.

3 Mandatory Configures the egress gateway of the DHCP


client.

6 Optional Specifies the IP address of the DNS server. If you


configure the domain name of the intermediate
file server (for example, www.ztp.com) as its
host name, deploy a DNS server to resolve the
domain name to the corresponding IP address. If
you configure the IP address of the intermediate
file server as its host name, you do not need to
deploy a DNS server.

7 Optional Specifies the IP address of the Syslog server.

66 Mandatory Specifies the user name and password for SSH


login. The format is as follows:
● ssh_switch://username:password

125 Mandatory Specifies the enterprise code and character


string.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 74


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

The lease of an IP address applied by a DHCP client is at least 1 hour.

In following procedures, CloudEngine switches function as the DHCP server or


DHCP relay agent. If a device of a different type serves as the DHCP server or
relay agent, see the documentation of the corresponding product for configuration
details.

Procedure
Step 1 Perform the following steps on the DHCP server:
1. Run the system-view command to enter the system view.
2. Run the dhcp enable command to enable DHCP.
3. Run the dhcp server request-packet all-interface enable command to
enable the DHCP server function on all interfaces. By default, the DHCP server
function is disabled on all interfaces.
4. Run the ip pool ip-pool-name command to create a global address pool and
enter its view.
5. Run the gateway-list ip-address &<1-8> command to set an egress gateway
address for DHCP clients.
6. Run the network ip-address [ mask { mask | mask-length } ] command to
specify the range of IP addresses to be allocated to DHCP clients.
7. Run the option code [ sub-option sub-code ] { ascii ascii-string | hex hex-
string | cipher cipher-string | ip-address ip-address &<1-8> } command to
configure DHCP Option 66. See Table 2-8 for DHCP options to be configured.
NOTE

A secure password should contain at least two types of the following: lowercase letters,
uppercase letters, digits, and special characters. In addition, the password must consist of
at least six characters.
8. Run the option 125 hex 00 00 07 DB 08 01 06 6D 6F 64 65 31 00 command
to configure Option 125.
9. Run the commit command to commit the configuration.
Step 2 (Optional) Perform the following steps on the DHCP relay agent:
If the unconfigured device and DHCP server are on different network segments,
configure a DHCP relay agent to forward DHCP packets exchanged between them.
1. Run the system-view command to enter the system view.
2. Run the dhcp enable command to enable DHCP.
3. Run the interface interface-type interface-number command to enter the
interface view.
4. On an Ethernet interface, run undo portswitch
The interface is switched to Layer 3 mode.
By default, an Ethernet interface works in Layer 2 mode.
The mode switching function takes effect when the interface only has
attribute configurations (for example, shutdown and description

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 75


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

configurations). Alternatively, if configuration information supported by both


Layer 2 and Layer 3 interfaces exists (for example, mode lacp and lacp
system-id configurations), no configuration that is not supported after the
working mode of the interface is switched can exist. If unsupported
configurations exist on the interface, delete the configurations first and then
run the undo portswitch command.

NOTE

If many Ethernet interfaces need to be switched to Layer 3 mode, run the undo
portswitch batch interface-type { interface-number1 [ to interface-number2 ] }
&<1-10> command in the system view to switch these interfaces to Layer 3 mode in
batches.
5. Run the ip address ip-address { mask | mask-length } command to configure
an IP address for the interface.
6. Run the dhcp select relay command to enable the DHCP relay function.
7. Run the dhcp relay binding server ip ip-address command to specify the
DHCP server IP address on the DHCP relay agent.
8. Run the commit command to commit the configuration.

----End

2.7.3 Configuring Automatic Deployment Through the


Management Interface

Context
DHCP uses the client/server model. A client applies to the server for configuration
parameters, such as an IP address. The server replies with the requested
configuration parameters to dynamically assign these parameters to the client.
After receiving Option 125 from the DHCP server, the SSH login user name and
password, SSH user rights, and maximum number of SSH login attempts are
automatically configured on the device, LLDP is enabled globally, and IPv6 is
enabled on the management interface.

Follow-up Procedure
After automatic device deployment is complete, the SSH management interface
that can be used for login is automatically configured, and the ZTP process exits.

2.8 Configuration Examples for ZTP


This section provides ZTP configuration examples, including the networking
requirements, configuration roadmap, and configuration procedure.

This section only provides configuration examples for individual features. For
details about multi-feature configuration examples, feature-specific configuration
examples, interoperation examples, protocol or hardware replacement examples,
and industry application examples, see the Typical Configuration Examples.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 76


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

2.8.1 Example for Configuring Unconfigured Devices to


Implement Automatic Deployment Using a USB Flash Drive

Networking Requirements
A new network needs to be deployed. SwitchA and SwitchB are two unconfigured
devices. The customer requires that SwitchA and SwitchB automatically load
system software and configuration files after they are powered on to reduce labor
costs and device deployment time.
Table 2-9 lists information about SwitchA and SwitchB, and files to be loaded on
the switches.

Table 2-9 Device information and files to be loaded


New Device Device Serial Number File to Be Loaded
Model

SwitchA CE12800 210235527210 ● System software: CE12800-


D4000028 V100R005C00.cc
● Configuration file:
conf_210235527210D40000
28.cfg

SwitchB CE12800 210235527210 ● System software: CE12800-


D4000046 V100R005C00.cc
● Configuration file:
conf_210235527210D40000
46.cfg

Configuration Roadmap
The configuration roadmap is as follows:
1. Edit the intermediate file ztp_config.ini to enable the switches to obtain their
system software packages and configuration files according to the
intermediate file.
2. Save the intermediate file and version files to the USB flash drive so that the
switches can implement automatic deployment using the USB flash drive.
3. Install the USB flash drive and power on the switches to start the ZTP process.

Procedure
Step 1 Edit the intermediate file.
Edit the intermediate file according to 2.2.2 Intermediate File in INI Format. The
file is named ztp_config.ini and has the following format:
;BEGIN DC
[GLOBAL CONFIG]
FILESERVER=file:/usb:/

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 77


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

[DEVICE0 DESCRIPTION]
ESN=210235527210D4000028
DEVICETYPE=CE12800
SYSTEM-SOFTWARE=CE12800-V100R005C00.cc
SYSTEM-CONFIG=conf_210235527210D4000028.cfg
[DEVICE1 DESCRIPTION]
ESN=210235527210D4000046
DEVICETYPE=CE12800
SYSTEM-SOFTWARE=CE12800-V100R005C00.cc
SYSTEM-CONFIG=conf_210235527210D4000046.cfg
;END DC

Step 2 Save the intermediate file and version files to the root directory of the USB flash
drive.
Step 3 Connect the USB flash drive to SwitchA and power on SwitchA.
Step 4 Verify the configuration.
# The switches complete the ZTP process 15 minutes after they are powered on.
Log in to the switches and run the display startup command to check whether
the current system software and configuration file are the required ones.
<SwitchA> display startup
MainBoard:
Configured startup system software: flash:/CE12800-V100R005C00.cc
Startup system software: flash:/CE12800-V100R005C00.cc
Next startup system software: flash:/CE12800-V100R005C00.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
SlaveBoard:
Configured startup system software: flash:/CE12800-V100R005C00.cc
Startup system software: flash:/CE12800-V100R005C00.cc
Next startup system software: flash:/CE12800-V100R005C00.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL

Step 5 After SwitchA completes automatic deployment, remove the USB flash drive and
connect the USB flash drive to SwitchB. Then power on SwitchB to start automatic
deployment.

----End

2.8.2 Example for Configuring Unconfigured Devices to


Implement Automatic Deployment Through DHCP

Context

Networking Requirements
As shown in Figure 2-4, SwitchA and SwitchB are two unconfigured switches on
the network, and both are connected to SwitchC. SwitchC functions as the egress
gateway of SwitchA and SwitchB. There are reachable routes between SwitchC and
the DHCP server, and between SwitchC and the file server.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 78


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

The customer requires that SwitchA and SwitchB automatically load system
software and configuration files after they are powered on to reduce labor costs
and device deployment time.
Table 2-10 lists information about SwitchA and SwitchB, and files to be loaded on
the switches.

Table 2-10 Device information and files to be loaded


New Device Device Serial Number File to Be Loaded
Model

SwitchA CE12800 210235527210 ● System software: CE12800-


D4000028 V200R019C10.cc
● Configuration file:
conf_210235527210D40000
28.cfg

SwitchB CE12800 210235527210 ● System software: CE12800-


D4000046 V200R019C10.cc
● Configuration file:
conf_210235527210D40000
46.cfg

Figure 2-4 Configuring ZTP

Configuration Roadmap
The configuration roadmap is as follows:
1. Configure a file server, and specify the file server as the SFTP server to store
the intermediate file, system software, and configuration file.
NOTE

SFTP is recommended for file transfer.


2. Edit the intermediate file ztp_script.py to enable the switches to obtain their
system software packages and configuration files according to the
intermediate file.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 79


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

3. Configure the DHCP server and relay agent to enable unconfigured switches
to obtain DHCP information.
4. Power on SwitchA and SwitchB to start the ZTP process.

Procedure
Step 1 Configure the file server. (The following example uses a PC as the file server. If a
device of a different type functions as the file server, configure the device
according to the corresponding operation guide.)
1. Configure the file server (PC) as an SFTP server. Run the SFTP server software
(for example, FileZilla) on the PC to implement the SFTP server function.
Choose Edit > Users. In the dialog box that is displayed, click Add, set the
user name to sftpuser, and select Password to set the password, as shown in
Figure 2-5.

Figure 2-5 Configuring the file server

As shown in Figure 2-6, select the Shared folders menu, click Add to set the
SFTP working directory on the PC to D:\ztp, click Set as home dir, and then
click OK to close the dialog box.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 80


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Figure 2-6 Configuring the file server

2. Configure the IP address and gateway of the file server. Ensure that the file
server and gateway of SwitchA and SwitchB have reachable routes to each
other.

After configuring the file server, save the system software and configuration files
to be loaded to switches in the D:\ztp directory.

Step 2 Edit the intermediate file.

Edit the intermediate file according to 2.2.3 Intermediate File in Python Format.
The file is named ztp_script.py. For the file contents, see ztp_script.py File and
Configuration Files.

After editing the intermediate file, save the file to the working directory D:\ztp on
the file server.

Step 3 Configure the DHCP server.

# Configure an IP address pool for the DHCP server to allocate IP addresses to


clients and configure Option values of the DHCP server according to Table 2-11.
In this example, a switch is used as the DHCP server.

Table 2-11 Option values of the DHCP server

Option Description Value


No.

1 Subnet mask of an 255.255.225.0


IP address

3 Egress gateway of 10.1.1.1


DHCP clients

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 81


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Option Description Value


No.

67 File server address sftp://sftpuser:Pwd123@10.1.3.2/


and intermediate ztp_script.py
file name

<HUAWEI> system-view
[~HUAWEI] sysname dhcp_server
[*HUAWEI] commit
[~dhcp_server] dhcp enable
[~dhcp_server] dhcp server request-packet all-interface enable
[*dhcp_server] ip pool pool1
[*dhcp_server-ip-pool-pool1] gateway-list 10.1.1.1
[*dhcp_server-ip-pool-pool1] network 10.1.1.0 mask 255.255.255.0
[*dhcp_server-ip-pool-pool1] option 67 cipher sftp://sftpuser:Pwd123@10.1.3.2/ztp_script.py
[*dhcp_server-ip-pool-pool1] quit

# Configure the IP address and gateway of the DHCP server. Ensure that the DHCP
server and gateway of SwitchA and SwitchB have reachable routes to each other.
Step 4 Configure the DHCP relay agent.
# On SwitchC, configure the DHCP relay function and set the IP address of the
interface connected to SwitchA and SwitchB to 10.1.1.1. The interface functions as
the default gateway of SwitchA and SwitchB.
<HUAWEI> system-view
[~HUAWEI] sysname SwitchC
[*HUAWEI] commit
[~SwitchC] vlan batch 10
[*SwitchC] interface 10ge 1/0/1
[*SwitchC-10GE1/0/1] port link-type trunk
[*SwitchC-10GE1/0/1] port trunk allow-pass vlan 10
[*SwitchC-10GE1/0/1] port trunk pvid vlan 10
[*SwitchC-10GE1/0/1] quit
[*SwitchC] interface 10ge 1/0/2
[*SwitchC-10GE1/0/2] port link-type trunk
[*SwitchC-10GE1/0/2] port trunk allow-pass vlan 10
[*SwitchC-10GE1/0/2] port trunk pvid vlan 10
[*SwitchC-10GE1/0/2] quit
[*SwitchC] interface vlanif 10
[*SwitchC-Vlanif10] ip address 10.1.1.1 24
[*SwitchC-Vlanif10] quit
[*SwitchC] dhcp enable
[*SwitchC] interface vlanif 10
[*SwitchC-Vlanif10] dhcp select relay
[*SwitchC-Vlanif10] dhcp relay binding server ip 10.1.2.2
[*SwitchC-Vlanif10] commit

Step 5 Power on SwitchA and SwitchB to start the ZTP process.


Step 6 Verify the configuration.
# The switches complete the ZTP process in about 15 minutes after they are
powered on. Log in to the switches and run the display startup command to
check whether the current system software and configuration files are the
required ones. Use SwitchA as an example.
<SwitchA> display startup
MainBoard:
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 82


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Next startup system software: flash:/CE12800-V200R019C10.cc


Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
SlaveBoard:
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc
Next startup system software: flash:/CE12800-V200R019C10.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL

----End

ztp_script.py File and Configuration Files


ztp_script.py File

NOTE

#sha256sum= is the SHA256 code of the intermediate file ztp_script.py. You can modify the
contents of ztp_script.py according to actual networking requirements. After the modification,
use an SHA256 calculation tool, such as sha256sum, to generate the SHA256 code of the
modified file.
The intermediate file cannot contain #sha256sum= when the SHA256 code is generated. Add
#sha256sum= to the beginning of the script file after the SHA256 code is generated.
This script file is used for reference only and can be transferred using SFTP. You can modify the
script file based on deployment requirements.

#sha256sum="7eb57f7cf11d2bf481e544f333267e8dd43a532c12a60151d640d64872013a8b"
#!/usr/bin/env python
#
# Copyright (C) Huawei Technologies Co., Ltd. 2008-2013. All rights reserved.
# ----------------------------------------------------------------------------------------------------------------------
# History:
# Date Author Modification
# 20130629 Author created file.
# ----------------------------------------------------------------------------------------------------------------------

"""
Zero Touch Provisioning (ZTP) enables devices to automatically load version files including system software,
patch files, configuration files when the device starts up, the devices to be configured must be new devices
or have no configuration files.

This is a sample of Zero Touch Provisioning user script. You can customize it to meet the requirements of
your network environment.
"""

import http.client
import urllib.request, urllib.parse, urllib.error
import string
import re
import xml.etree.ElementTree as etree
import os
import stat
import logging
import traceback
import hashlib
import sys

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 83


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

from urllib.parse import urlparse


from urllib.parse import urlunparse
from time import sleep

# error code
OK =0
ERR =1

# File server in which stores the necessary system software, configuration and patch files:
# 1) Specify the file server which supports the following format.
# sftp://[username[:password]@]hostname[:port]
# 2) Do not add a trailing slash at the end of file server path.
FILE_SERVER = 'sftp://sftpuser:Pwd123@10.1.3.2'

# Remote file paths:


# 1) The path may include directory name and file name.
# 2) If file name is not specified, indicate the procedure can be skipped.
# File paths of system software on file server, filename extension is '.cc'.
REMOTE_PATH_IMAGE = {
'CE12800' : '/CE6800-V200R019C10.cc',}
# File path of configuration file on file server, filename extension is '.cfg', '.zip' or '.dat'.
REMOTE_PATH_CONFIG = '/conf_%s.cfg'
# File path of patch file on file server, filename extension is '.pat'
REMOTE_PATH_PATCH = {
'CE12800' : '',
}
# File path of stack member ID file on file server, filename extension is '.txt'
REMOTE_PATH_MEMID = ''
# File path of license list file, filename extension is '.xml'
REMOTE_PATH_LICLIST = ''
# File path of sha256 file, contains sha256 value of image / patch / memid / license file, file extension is
'.txt'
REMOTE_PATH_SHA256 = ''
# File path of python file on file server, filename extension is '.py'
REMOTE_PATH_PYTHON = ''

# Max times to retry get startup when no query result


GET_STARTUP_INTERVAL = 15 # seconds
MAX_TIMES_GET_STARTUP = 120 # Max times to retry

# Max times to retry when download file faild


MAX_TIMES_RETRY_DOWNLOAD = 3

class OPSConnection(object):
"""Make an OPS connection instance."""

def __init__(self, host, port = 80):


self.host = host
self.port = port
self.headers = {
"Content-type": "application/xml",
"Accept": "application/xml"
}

self.conn = http.client.HTTPConnection(self.host, self.port)

def close(self):
"""Close the connection"""
self.conn.close()

def create(self, uri, req_data):


"""Create a resource on the server"""
ret = self._rest_call("POST", uri, req_data)
return ret

def delete(self, uri, req_data):


"""Delete a resource on the server"""

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 84


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

ret = self._rest_call("DELETE", uri, req_data)


return ret

def get(self, uri, req_data = None):


"""Retrieve a resource from the server"""
ret = self._rest_call("GET", uri, req_data)
return ret

def set(self, uri, req_data):


"""Update a resource on the server"""
ret = self._rest_call("PUT", uri, req_data)
return ret

def _rest_call(self, method, uri, req_data):


"""REST call"""
if req_data == None:
body = ""
else:
body = req_data

logging.info('HTTP request: %s %s HTTP/1.1', method, uri)


self.conn.request(method, uri, body, self.headers)
response = self.conn.getresponse()
ret = (response.status, response.reason, response.read())
if response.status != http.client.OK:
logging.info('%s', body)
logging.error('HTTP response: HTTP/1.1 %s %s\n%s', ret[0], ret[1], ret[2])
return ret

class OPIExecError(Exception):
"""OPI executes error."""
pass

class ZTPErr(Exception):
"""ZTP error."""
pass

def get_addr_by_hostname(ops_conn, host, addr_type = '1'):


"""Translate a host name to IPv4 address format. The IPv4 address is returned as a string."""
logging.info("Get IP address by host name...")
uri = "/dns/dnsNameResolution"
root_elem = etree.Element('dnsNameResolution')
etree.SubElement(root_elem, 'host').text = host
etree.SubElement(root_elem, 'addrType').text = addr_type
req_data = etree.tostring(root_elem, "UTF-8")
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to get address by host name')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "ipv4Addr", namespaces)
if elem is None:
raise OPIExecError('Failed to get IP address by host name')

return elem.text

def _del_rsa_peer_key(ops_conn, key_name):


"""Delete RSA peer key configuration"""
logging.info("Delete RSA peer key %s", key_name)
uri = "/rsa/rsaPeerKeys/rsaPeerKey"
root_elem = etree.Element('rsaPeerKey')
etree.SubElement(root_elem, 'keyName').text = key_name
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 85


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if ret != http.client.OK:
raise OPIExecError('Failed to delete RSA peer key')

except Exception as reason:


logging.error(reason)

def _del_sshc_rsa_key(ops_conn, server_name, key_type = 'RSA'):


"""Delete SSH client RSA key configuration"""
logging.debug("Delete SSH client RSA key for %s", server_name)
uri = "/sshc/sshCliKeyCfgs/sshCliKeyCfg"
root_elem = etree.Element('sshCliKeyCfg')
etree.SubElement(root_elem, 'serverName').text = server_name
etree.SubElement(root_elem, 'pubKeyType').text = key_type
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete SSH client RSA key')

except Exception as reason:


logging.error(reason)

_del_rsa_peer_key(ops_conn, server_name)

def _set_sshc_first_time(ops_conn, switch):


"""Set SSH client attribute of authenticating user for the first time access"""
if switch not in ['Enable', 'Disable']:
return ERR

logging.info('Set SSH client first-time enable switch = %s', switch)


uri = "/sshc/sshClient"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshClient>
<firstTimeEnable>$enable</firstTimeEnable>
</sshClient>
''')
req_data = str_temp.substitute(enable = switch)
ret, _, _ = ops_conn.set(uri, req_data)
if ret != http.client.OK:
if switch == 'Enable':
raise OPIExecError('Failed to enable SSH client first-time')
else:
raise OPIExecError('Failed to disable SSH client first-time')

return OK

def _sftp_download_file(ops_conn, url, local_path):


"""Download file using SFTP."""
_set_sshc_first_time(ops_conn, 'Enable')

logging.info('SFTP download "%s" to "%s".', url, local_path)


uri = "/sshc/sshcConnects/sshcConnect"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshcConnect>
<HostAddrIPv4>$serverIp</HostAddrIPv4>
<commandType>get</commandType>
<userName>$username</userName>
<password>$password</password>
<localFileName>$localPath</localFileName>
<remoteFileName>$remotePath</remoteFileName>
<identityKey>ssh-rsa</identityKey>
<transferType>SFTP</transferType>
</sshcConnect>
''')
url_tuple = urlparse(url)
if re.match(r"\d+\.\d+\.\d+\.\d+", url_tuple.hostname):
server_ip = url_tuple.hostname

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 86


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

else:
server_ip = get_addr_by_hostname(ops_conn, url_tuple.hostname)
req_data = str_temp.substitute(serverIp = server_ip, username = url_tuple.username, password =
url_tuple.password,
remotePath = url_tuple.path[1:], localPath = local_path)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
print(('Failed to download file "%s" using SFTP' % os.path.basename(local_path)))
sys.stdout.flush()
ret = ERR
else:
ret = OK

_del_sshc_rsa_key(ops_conn, server_ip)
_set_sshc_first_time(ops_conn, 'Disable')
return ret

def _usb_download_file(ops_conn, url, local_path):


"""Download file using usb"""
logging.info('USB download "%s" to "%s".', url, local_path)

url_tuple = urlparse(url, allow_fragments=False)


src_path = url_tuple.path[1:]
try:
copy_file(ops_conn, src_path, local_path)
except:
print(('Failed to download file "%s" using USB' % os.path.basename(local_path)))
sys.stdout.flush()
return ERR
return OK

def download_file(ops_conn, url, local_path, retry_times = 0):


"""Download file, support SFTP.

sftp://[username[:password]@]hostname[:port]/path

Args:
ops_conn: OPS connection instance
url: URL of remote file
local_path: local path to put the file

Returns:
A integer of return code
"""
url_tuple = urlparse(url)
print(("Info: Download %s to %s" % (url_tuple.path[1:], local_path)))
sys.stdout.flush()
func_dict = {'sftp': _sftp_download_file,
'file': _usb_download_file}
scheme = url_tuple.scheme
if scheme not in list(func_dict.keys()):
raise ZTPErr('Unknown file transfer scheme %s' % scheme)

ret = OK
cnt = 0
while (cnt < 1 + retry_times):
if cnt:
print('Retry downloading...')
sys.stdout.flush()
logging.info('Retry downloading...')
ret = func_dict[scheme](ops_conn, url, local_path)
if ret is OK:
break
cnt += 1

if ret is not OK:


raise ZTPErr('Failed to download file "%s"' % os.path.basename(url))

return OK

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 87


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

class StartupInfo(object):
"""Startup configuration information

image: startup system software


config: startup saved-configuration file
patch: startup patch package
"""
def __init__(self, image = None, config = None, patch = None):
self.image = image
self.config = config
self.patch = patch

class Startup(object):
"""Startup configuration information

current: current startup configuration


next: current next startup configuration
"""
def __init__(self, ops_conn):
self.ops_conn = ops_conn
self.current, self.next = self._get_startup_info()

def _get_startup_info(self):
"""Get the startup information."""
logging.info("Get the startup information...")
uri = "/cfg/startupInfos/startupInfo"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<startupInfo>
<position/>
<configedSysSoft/>
<curSysSoft/>
<nextSysSoft/>
<curStartupFile/>
<nextStartupFile/>
<curPatchFile/>
<nextPatchFile/>
</startupInfo>'''

cnt = 0
while (cnt < MAX_TIMES_GET_STARTUP):
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
cnt += 1
logging.warning('Failed to get the startup information')
continue

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
mpath = 'data' + uri.replace('/', '/vrp:') # match path
nslen = len(namespaces['vrp'])
elem = root_elem.find(mpath, namespaces)
if elem is not None:
break
logging.warning('No query result while getting startup info')
sleep(GET_STARTUP_INTERVAL) # sleep to wait for system ready when no query result
cnt += 1

if elem is None:
raise OPIExecError('Failed to get the startup information')

current = StartupInfo() # current startup info


curnext = StartupInfo() # next startup info
for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}text'
if tag == 'curSysSoft':
current.image = child.text
elif tag == 'nextSysSoft':

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 88


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

curnext.image = child.text
elif tag == 'curStartupFile' and child.text != 'NULL':
current.config = child.text
elif tag == 'nextStartupFile' and child.text != 'NULL':
curnext.config = child.text
elif tag == 'curPatchFile' and child.text != 'NULL':
current.patch = child.text
elif tag == 'nextPatchFile' and child.text != 'NULL':
curnext.patch = child.text
else:
continue

return current, curnext

def _set_startup_image_file(self, file_path):


"""Set the next startup system software"""
logging.info("Set the next startup system software to %s...", file_path)
uri = "/sum/startupbymode"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startupbymode>
<softwareName>$fileName</softwareName>
<mode>STARTUP_MODE_ALL</mode>
</startupbymode>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup system software")

def _set_startup_config_file(self, file_path):


"""Set the next startup saved-configuration file"""
logging.info("Set the next startup saved-configuration file to %s...", file_path)
uri = "/cfg/setStartup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<setStartup>
<fileName>$fileName</fileName>
</setStartup>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup configuration file")

def _del_startup_config_file(self):
"""Delete startup config file"""
logging.info("Delete the next startup config file...")
uri = "/cfg/clearStartup"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<clearStartup>
</clearStartup>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to delete startup configuration file")

def _set_startup_patch_file(self, file_path):


"""Set the next startup patch file"""
logging.info("Set the next startup patch file to %s...", file_path)
uri = "/patch/startup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startup>
<packageName>$fileName</packageName>
</startup>

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 89


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup patch file")

def _get_cur_stack_member_id(self):
"""rest api: Get current stack member id"""

logging.info("Get current stack member ID...")


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID></memberID>
</stackMemberInfo>
'''
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get current stack member id, rsp not ok')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "memberID", namespaces)
if elem is None:
raise OPIExecError('Failed to get the current stack member id for no "memberID" element')

return elem.text

def _set_stack_member_id(self, file_path, esn):


"""Set the next stack member ID"""

def get_stackid_from_file(fname, esn):


"""parse esn_id.txt file and get stack id according to esn num
format of esn_stackid file is like below:

sn Irf group Irf number


Sdddg 100 1
Sddde 100 2
"""
# fname must exist, guaranteed by caller
fname = os.path.basename(fname)
with open(fname, 'r') as item:
for line in item:
token = line.strip('[\r\n]')
token = token.split()
if token[0] == esn:
return token[2]
return None

logging.info('Set the next stack member ID, filename %s', file_path)


uri = "/stack/stackMemberInfos/stackMemberInfo"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>$curmemberid</memberID>
<nextMemberID>$memberid</nextMemberID>
</stackMemberInfo>
''')

cur_memid = self._get_cur_stack_member_id()
next_memid = get_stackid_from_file(file_path, esn)
if not next_memid:
logging.error('Failed to get stack id from %s, esn %s', file_path, esn)
return

req_data = str_temp.substitute(curmemberid = cur_memid, memberid = next_memid)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 90


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

ret, _, _ = self.ops_conn.set(uri, req_data)


if ret != http.client.OK:
raise OPIExecError('Failed to set stack id {}'.format(next_memid))

return OK

def _reset_stack_member_id(self):
"""rest api: reset stack member id"""

logging.info('Reset the next stack member ID')


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>1</memberID>
<nextMemberID>1</nextMemberID>
</stackMemberInfo>
'''

ret, _, _ = self.ops_conn.set(uri, req_data)


if ret != http.client.OK:
raise OPIExecError('Failed to reset stack id ')

return OK

def _reset_startup_patch_file(self):
"""Rest patch file for system to startup"""
logging.info("Reset the next startup patch file...")
uri = "/patch/resetpatch"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<resetpatch>
</resetpatch>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to reset patch')

def reset_startup_info(self, slave):


"""Reset startup info and delete the downloaded files"""
logging.info("Reset the next startup information...")
_, configured = self._get_startup_info()

# 1. Reset next startup config file and delete it


try:
if configured.config != self.next.config:
if self.next.config is None:
self._del_startup_config_file()
else:
self._set_startup_config_file(self.next.config)
if configured.config is not None:
del_file_all(self.ops_conn, configured.config, slave)

except Exception as reason:


logging.error(reason)

# 2. Reset next startup patch file


try:
if configured.patch != self.next.patch:
if self.next.patch is None:
self._reset_startup_patch_file()
else:
self._set_startup_patch_file(self.next.patch)

if configured.patch is not None:


del_file_all(self.ops_conn, configured.patch, slave)
except Exception as reason:
logging.error(reason)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 91


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# 3. Reset next startup system software and delete it


try:
if configured.image != self.next.image:
self._set_startup_image_file(self.next.image)
del_file_all(self.ops_conn, configured.image, slave)
except Exception as reason:
logging.error(reason)

# 4. reset stack member id


try:
self._reset_stack_member_id()
except Exception as reason:
logging.error(reason)

def set_startup_info(self, image_file, config_file, patch_file, memid_file, slave, esn_str):


"""Set the next startup information."""
logging.info("Set the next startup information...")
# 1. Set next startup system software
if image_file is not None:
try:
self._set_startup_image_file(image_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, image_file, slave)
self.reset_startup_info(slave)
raise

# 2. Set next startup config file


if config_file is not None:
try:
self._set_startup_config_file(config_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, config_file, slave)
self.reset_startup_info(slave)
raise

# 3. Set next startup patch file


if patch_file is not None:
try:
self._set_startup_patch_file(patch_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, patch_file, slave)
self.reset_startup_info(slave)
raise

# 4. Set next member id


if memid_file is not None:
try:
self._set_stack_member_id(memid_file, esn_str)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, memid_file, None)
self.reset_startup_info(slave)
raise

def get_cwd(ops_conn):
"""Get the full filename of the current working directory"""
logging.info("Get the current working directory...")
uri = "/vfm/pwds/pwd"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<pwd>
<dictionaryName/>
</pwd>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 92


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

raise OPIExecError('Failed to get the current working directory')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "dictionaryName", namespaces)
if elem is None:
raise OPIExecError('Failed to get the current working directory for no "directoryName" element')

return elem.text

def file_exist(ops_conn, file_path):


"""Returns True if file_path refers to an existing file, otherwise returns False"""
uri = "/vfm/dirs/dir"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<dir>
<fileName>$fileName</fileName>
</dir>
''')
req_data = str_temp.substitute(fileName = file_path)
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to list information about the file "%s"' % file_path)

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "fileName", namespaces)
if elem is None:
return False

return True

def del_file(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>
</deleteFileUnRes>
''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete the file "%s" permanently' % file_path)

except Exception as reason:


logging.error(reason)

def del_file_noerror(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>
</deleteFileUnRes>

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 93


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.info("file not exist")

except Exception as reason:


logging.error(reason)

def del_file_all(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file(ops_conn, file_path)
if slave:
del_file(ops_conn, 'slave#' + file_path)

def del_file_all_noerror(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file_noerror(ops_conn, file_path)
if slave:
del_file_noerror(ops_conn, 'slave#' + file_path)

def copy_file(ops_conn, src_path, dest_path):


"""Copy a file"""
print(('Info: Copy file %s to %s...' % (src_path, dest_path)))
sys.stdout.flush()
logging.info('Copy file %s to %s...', src_path, dest_path)
uri = "/vfm/copyFile"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<copyFile>
<srcFileName>$src</srcFileName>
<desFileName>$dest</desFileName>
</copyFile>
''')
req_data = str_temp.substitute(src = src_path, dest = dest_path)

# it is a action operation, so use create for HTTP POST


ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to copy "%s" to "%s"' % (src_path, dest_path))

def has_slave_mpu(ops_conn):
"""Whether device has slave MPU, returns a bool value"""
logging.info("Test whether device has slave MPU...")
uri = "/devm/phyEntitys"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<phyEntitys>
<phyEntity>
<entClass>mpuModule</entClass>
<entStandbyState/>
<position/>
</phyEntity>
</phyEntitys>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the device slave information')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
for entity in root_elem.findall(uri + 'phyEntity', namespaces):
elem = entity.find("vrp:entStandbyState", namespaces)
if elem is not None and elem.text == 'slave':

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 94


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

return True

return False

def get_system_info(ops_conn):
"""Get system info, returns a dict"""
logging.info("Get the system information...")
uri = "/system/systemInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<systemInfo>
<productName/>
<esn/>
<mac/>
</systemInfo>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the system information')

sys_info = {}.fromkeys(('productName', 'esn', 'mac'))


root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:')
nslen = len(namespaces['vrp'])
elem = root_elem.find(uri, namespaces)
if elem is not None:
for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}esn'
if tag in list(sys_info.keys()):
sys_info[tag] = child.text

return sys_info

def test_file_paths(image, config, patch, stack_memid, sha256_file, license_list_file):


"""Test whether argument paths are valid."""
logging.info("Test whether argument paths are valid...")
# check image file path
file_name = os.path.basename(image)
if file_name != '' and not file_name.lower().endswith('.cc'):
print('Error: Invalid filename extension of system software')
sys.stdout.flush()
return False

# check config file path


file_name = os.path.basename(config)
file_name = file_name.lower()
_, ext = os.path.splitext(file_name)
if file_name != '' and ext not in ['.cfg', '.zip', '.dat']:
print('Error: Invalid filename extension of configuration file')
sys.stdout.flush()
return False

# check patch file path


file_name = os.path.basename(patch)
if file_name != '' and not file_name.lower().endswith('.pat'):
print('Error: Invalid filename extension of patch file')
sys.stdout.flush()
return False

# check stack member id file path


file_name = os.path.basename(stack_memid)
if file_name != '' and not file_name.lower().endswith('.txt'):
print('Error: Invalid filename extension of stack member ID file')
sys.stdout.flush()
return False

# check sha256 file path


file_name = os.path.basename(sha256_file)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 95


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if file_name != '' and not file_name.lower().endswith('.txt'):


print('Error: Invalid filename extension of sha256 file')
sys.stdout.flush()
return False

# check license list file path


file_name = os.path.basename(license_list_file)
if file_name != '' and not file_name.lower().endswith('.xml'):
print('Error: Invalid filename extension of license list file')
sys.stdout.flush()
return False

return True

def sha256sum(fname, need_skip_first_line = False):


"""
Calculate sha256 num for this file.
"""

def read_chunks(fhdl):
'''read chunks'''
chunk = fhdl.read(8096)
while chunk:
yield chunk
chunk = fhdl.read(8096)
else:
fhdl.seek(0)

sha256_obj = hashlib.sha256()
if isinstance(fname, str) and os.path.exists(fname):
with open(fname, "rb") as fhdl:
#skip the first line
fhdl.seek(0)
if need_skip_first_line:
fhdl.readline()
for chunk in read_chunks(fhdl):
sha256_obj.update(chunk)
elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file):
for chunk in read_chunks(fname):
sha256_obj.update(chunk)
else:
pass
return sha256_obj.hexdigest()

def sha256_get_from_file(fname):
"""Get sha256 num form file, stored in first line"""

with open(fname, "r") as fhdl:


fhdl.seek(0)
line_first = fhdl.readline()

# if not match pattern, the format of this file is not supported


if not re.match('^#sha256sum="[\\w]{64}"[\r\n]+$', line_first):
return 'None'

return line_first[12:76]

def sha256_check_with_first_line(fname):
"""Validate sha256 for this file"""

fname = os.path.basename(fname)
sha256_calc = sha256sum(fname, True)
sha256_file = sha256_get_from_file(fname)

if sha256_file.lower() != sha256_calc:
logging.warning('sha256 check failed, file %s', fname)
print(('sha256 checksum of the file "%s" is %s' % (fname, sha256_calc)))
sys.stdout.flush()
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256_calc)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 96


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

print(('sha256 checksum received from the file "%s" is %s' % (fname, sha256_file)))
sys.stdout.flush()
logging.warning('sha256 checksum received from the file "%s" is %s', fname, sha256_file)
return False

return True

def sha256_check_with_dic(sha256_dic, fname):


"""sha256 check with dic"""
if fname not in sha256_dic:
logging.info('sha256_dic does not has key %s, no need to do sha256 verification', fname)
return True

sha256sum_result = sha256sum(fname, False)


if sha256_dic[fname] == sha256sum_result:
return True

print(('sha256 checksum of the file "%s" is %s' % (fname, sha256sum_result)))


sys.stdout.flush()
print(('sha256 checksum received for the file "%s" is %s' % (fname, sha256_dic[fname])))
sys.stdout.flush()
logging.warning('sha256 check failed, file %s', fname)
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256sum_result)
logging.warning('sha256 checksum received for the file "%s" is %s', fname, sha256_dic[fname])

return False

def parse_sha256_file(fname):
"""parse sha256 file"""

def read_line(fhdl):
"""read a line by loop"""
line = fhdl.readline()
while line:
yield line
line = fhdl.readline()
else:
fhdl.seek(0)

sha256_dic = {}
with open(fname, "rb") as fhdl:
for line in read_line(fhdl):
line_spilt = line.split()
if 2 != len(line_spilt):
continue
dic_tmp = {line_spilt[0]: line_spilt[1]}
sha256_dic.update(dic_tmp)
return sha256_dic

def verify_and_parse_sha256_file(fname):
"""
vefiry data integrity of sha256 file and parse this file

format of this file is like:


------------------------------------------------------------------
#sha256sum="517cf194e2e1960429c6aedc0e4dba37"

file-name sha256
conf_5618642831132.cfg c0ace0f0542950beaacb39cd1c3b5716
------------------------------------------------------------------
"""
if not sha256_check_with_first_line(fname):
return ERR, None
return OK, parse_sha256_file(fname)

def check_parameter(aset):
seq = ['&', '>', '<', '"', "'"]
if aset:
for c in seq:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 97


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if c in aset:
return True
return False

def check_filename(ops_conn):
sys_info = get_system_info(ops_conn)
url_tuple = urlparse(FILE_SERVER)
if check_parameter(url_tuple.username) or check_parameter(url_tuple.password):
raise ZTPErr('Invalid username or password, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of system software, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_CONFIG)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of configuration file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_PATCH.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of patch file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_MEMID)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of member ID file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_SHA256)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of sha256 file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_LICLIST)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of license list file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
return OK

def active_license(ops_conn, license_name):


if license_name:
uri = "/lcs/lcsActive"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<lcsActive>
<lcsFileName>$lcsFileName</lcsFileName>
</lcsActive>
''')
req_data = str_temp.substitute(lcsFileName = license_name)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.error('Error: Failed to active license.')
return ERR
return OK

def main_proc(ops_conn):
"""Main processing"""
sys_info = get_system_info(ops_conn) # Get system info, such as esn and system mac
cwd = get_cwd(ops_conn) # Get the current working directory
startup = Startup(ops_conn)
slave = has_slave_mpu(ops_conn) # Check whether slave MPU board exists or not
chg_flag = False

check_filename(ops_conn)

# check remote file paths


if not test_file_paths(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''), REMOTE_PATH_CONFIG,
REMOTE_PATH_PATCH.get(sys_info['productName'], ''), REMOTE_PATH_MEMID,
REMOTE_PATH_SHA256,
REMOTE_PATH_LICLIST):
return ERR

# download sha256 file first, used to verify data integrity of files which will be downloaded next
local_path_sha256 = None
file_path = REMOTE_PATH_SHA256
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 98


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

url = FILE_SERVER + file_path


local_path_sha256 = cwd + file_name
ret = download_file(ops_conn, url, local_path_sha256, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download sha256 file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download sha256 file successfully')
sys.stdout.flush()
ret, sha256_dic = verify_and_parse_sha256_file(file_name)
# delete the file immediately
del_file_all(ops_conn, local_path_sha256, None)
if ret is ERR:
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
return ERR
else:
sha256_dic = {}

# download configuration file


local_path_config = None
file_path = REMOTE_PATH_CONFIG
if "%s" in file_path:
file_path = REMOTE_PATH_CONFIG % sys_info['esn']
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_config = cwd + file_name
del_file_noerror(ops_conn, local_path_config)
ret = download_file(ops_conn, url, local_path_config, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download configuration file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download configuration file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_config)
copy_file(ops_conn, local_path_config, 'slave#' + local_path_config)
chg_flag = True

# download patch file


local_path_patch = None
file_path = REMOTE_PATH_PATCH.get(sys_info['productName'], '')
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if startup.current.patch:
cur_pat = os.path.basename(startup.current.patch).lower()
else:
cur_pat = ''
if file_name != '' and file_name.lower() != cur_pat:
url = FILE_SERVER + file_path
local_path_patch = cwd + file_name
del_file_noerror(ops_conn, local_path_patch)
ret = download_file(ops_conn, url, local_path_patch, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download patch file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
print('Info: Download patch file successfully')

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 99


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_patch)
copy_file(ops_conn, local_path_patch, 'slave#' + local_path_patch)
chg_flag = True

# download stack member ID file


local_path_memid = None
file_path = REMOTE_PATH_MEMID
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_memid = cwd + file_name
del_file_noerror(ops_conn,local_path_memid)
ret = download_file(ops_conn, url, local_path_memid, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
return ERR
print('Info: Download stack member ID file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
chg_flag = True
#no need copy to slave board

# download system software


local_path_image = None
file_path = REMOTE_PATH_IMAGE.get(sys_info['productName'], '')
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if startup.current.image:
cur_image = os.path.basename(startup.current.image).lower()
else:
cur_image = ''
if file_name != '' and file_name.lower() != cur_image:
url = FILE_SERVER + file_path
local_path_image = cwd + file_name
del_file_noerror(ops_conn, local_path_image)
ret = download_file(ops_conn, url, local_path_image, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
if file_exist(ops_conn, file_name):
del_file_all(ops_conn, local_path_image, slave)
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
print('Info: Download system software file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 100


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_image)
copy_file(ops_conn, local_path_image, 'slave#' + local_path_image)
chg_flag = True
# download license list file
local_path_liclist = None
file_path = REMOTE_PATH_LICLIST
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
download_space = os.path.dirname(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_liclist = cwd + file_name
del_file_noerror(ops_conn, local_path_liclist)
ret = download_file(ops_conn, url, local_path_liclist, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license list file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download license list file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
chg_flag = True

#execute license list file to get license file name which end with .dat
license_name = None
if local_path_liclist is not None:
tree = etree.parse(file_name)
root = tree.getroot()
for child in root.findall('Lic'):
name = child.get('name')
esn = child.find('Esn').text
if sys_info['esn'] in esn:
license_name = name
print(('Info: License file name is "%s"' % license_name))
sys.stdout.flush()
break
if license_name == None :
print('Warning: Esn of this device is not in the license list file')
sys.stdout.flush()
#del_file_all(ops_conn, local_path_config, slave)
#del_file_all(ops_conn, local_path_patch, slave)
#del_file_all(ops_conn, local_path_memid, slave)
#del_file_all(ops_conn, local_path_image, slave)
#del_file_all(ops_conn, local_path_liclist, slave)
#return ERR

# download license file


local_path_license = None
file_path = license_name
if file_path is not None:
if not file_path.startswith('/'):
file_path = '/' + file_path
file_path = download_space + file_path

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 101


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_license = cwd + file_name
ret = download_file(ops_conn, url, local_path_license, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
print('Info: Download license file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR
chg_flag = True
#no need copy to slave board

# download python file


local_path_python = None
file_path = REMOTE_PATH_PYTHON
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_python = cwd + file_name
del_file_noerror(ops_conn, local_path_python)
ret = download_file(ops_conn, url, local_path_python, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download python file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR
print('Info: Download python file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_python)
copy_file(ops_conn, local_path_python, 'slave#' + local_path_python)
chg_flag = True

if chg_flag is False:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 102


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

return ERR

# active license file


if local_path_license is not None:
ret = active_license(ops_conn, local_path_license)
if ret is ERR:
print('Info: Active license failed')
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
print(('Info: Active license sucessfully, name: %s' % local_path_license))
sys.stdout.flush()
sleep(10)
# set startup info
startup.set_startup_info(local_path_image, local_path_config, local_path_patch,
local_path_memid, slave, sys_info['esn'])

# delete stack member ID file and license list file after used
del_file_all(ops_conn, local_path_memid, None)
del_file_all(ops_conn, local_path_liclist, None)

return OK

def main(usb_path = ''):


"""The main function of user script. It is called by ZTP frame, so do not remove or change this function.

Args:
Raises:
Returns: user script processing result
"""
host = "localhost"
if usb_path and len(usb_path):
logging.info('ztp_script usb_path: %s', usb_path)
global FILE_SERVER
FILE_SERVER = 'file:///' + usb_path
try:
# Make an OPS connection instance.
ops_conn = OPSConnection(host)
ret = main_proc(ops_conn)

except OPIExecError as reason:


logging.error('OPI execute error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except ZTPErr as reason:


logging.error('ZTP error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except IOError as reason:


print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except Exception as reason:


logging.error(reason)
traceinfo = traceback.format_exc()
logging.debug(traceinfo)
ret = ERR

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 103


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

finally:
# Close the OPS connection
ops_conn.close()

return ret

if __name__ == "__main__":
main()

Configuration Files
● Configuration file of SwitchC
#
sysname SwitchC
#
vlan batch 10
#
dhcp enable
#
interface Vlanif10
ip address 10.1.1.1 255.255.255.0
dhcp select relay
dhcp relay binding server ip 10.1.2.2
#
interface 10GE1/0/1
port link-type trunk
port trunk pvid vlan 10
port trunk allow-pass vlan 10
#
interface 10GE1/0/2
port link-type trunk
port trunk pvid vlan 10
port trunk allow-pass vlan 10
#
return
● Configuration file of the DHCP server
#
sysname dhcp_server
#
dhcp enable
#
ip pool pool1
gateway-list 10.1.1.1
network 10.1.1.0 mask 255.255.255.0
option 67 cipher %^%#,nl-3C^(L"r2cE=]>Z[X2Xo+<e0-S;@s"#ReXBA(h>4\4h_@P']"!t4*26
):0x31:fqp7Jz4FG'SYLo#%^%#
#
return

2.8.3 Example for Configuring Unconfigured Devices to


Implement Automatic Deployment and Set up a Stack
Through DHCP

Networking Requirements
As shown in Figure 2-7, SwitchA and SwitchB are two unconfigured switches on
the network, and both are connected to SwitchC. SwitchC functions as the egress
gateway of SwitchA and SwitchB. There are reachable routes between SwitchC and
the DHCP server, and between SwitchC and the file server.
To reduce labor costs and device deployment time, the customer requires that
SwitchA and SwitchB can automatically load system software and configuration

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 104


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

file after they are powered on and set up a stack after automatic deployment is
complete.
Table 2-12 lists information about SwitchA and SwitchB, and files to be loaded on
the switches.

Table 2-12 Device information and files to be loaded


New Device Device Serial Number File to Be Loaded
Model

SwitchA CE12800 210235527210 ● System software: CE12800-


D4000028 V200R019C10.cc
● Configuration file:
conf_210235527210D40000
28.cfg

SwitchB CE12800 210235527210 ● System software: CE12800-


D4000046 V200R019C10.cc
● Configuration file:
conf_210235527210D40000
46.cfg

NOTE

To ensure that SwitchA and SwitchB can set up a stack, the following conditions must be met:
● SwitchA and SwitchB have been connected using stack cables before they are powered on.
● The configuration file must contain all stack configurations (including the stack priority,
stack domain ID, stack connection mode, and stack port) related to the stack member ID
to ensure that a stack can be set up successfully after the configuration file is loaded. For
example, the configuration file conf_210235527210D4000046.cfg must contain the stack
configurations related to stack member ID 2.

Figure 2-7 Configuring ZTP

Configuration Roadmap
The configuration roadmap is as follows:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 105


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

1. Configure a file server, and specify the file server as the SFTP server to store
the intermediate file, system software, and configuration file.
NOTE

SFTP is recommended for file transfer.


2. Edit the intermediate file ztp_script.py and the stack member ID file
stack_memberid.txt so that the switches can obtain their system software
packages and configuration files according to the intermediate file, and
obtain stack member IDs according to the stack member ID file.
3. Configure the DHCP server and relay agent to enable unconfigured switches
to obtain DHCP information.
4. Power on SwitchA and SwitchB to start the ZTP process.

Procedure
Step 1 Configure the file server. (The following example uses a PC as the file server. If a
device of a different type functions as the file server, configure the device
according to the corresponding operation guide.)
1. Configure the file server (PC) as an SFTP server. Run the SFTP server software
(for example, FileZilla) on the PC to implement the SFTP server function.
Choose Edit > Users. In the dialog box that is displayed, click Add, set the
user name to sftpuser, and select Password to set the password, as shown in
Figure 2-8.

Figure 2-8 Configuring the file server

As shown in Figure 2-9, select the Shared folders menu, click Add to set the
SFTP working directory on the PC to D:\ztp, click Set as home dir, and then
click OK to close the dialog box.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 106


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Figure 2-9 Configuring the file server

2. Configure the IP address and gateway of the file server. Ensure that the file
server and gateway of SwitchA and SwitchB have reachable routes to each
other.
After configuring the file server, save the system software and configuration files
to be loaded to switches in the D:\ztp directory.
Step 2 Edit the intermediate file.
Edit the intermediate file according to 2.2.3 Intermediate File in Python Format.
The file is named ztp_script.py. For the file contents, see ztp_script.py File and
Configuration Files.
After editing the intermediate file, save the file to the working directory D:\ztp on
the file server.
Step 3 Edit the stack member ID file.
Write stack member IDs of SwitchA and SwitchB into stack_memberid.txt in the
following format:
ESN Stack group Stack member
210235527210D4000028 10 1
210235527210D4000046 10 2

ESN is the equipment serial number, and Stack member indicates the stack
member ID of the switch.
After editing the stack member ID file, save the file to the working directory D:
\ztp on the file server.
Step 4 Configure the DHCP server.
# Configure an IP address pool for the DHCP server to allocate IP addresses to
clients and configure Option values of the DHCP server according to Table 2-13.
In this example, a switch is used as the DHCP server.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 107


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

Table 2-13 Option values of the DHCP server

Option Description Value


No.

1 Subnet mask of an 255.255.225.0


IP address

3 Egress gateway of 10.1.1.1


DHCP clients

67 File server address sftp://sftpuser:Pwd123@10.1.3.2/


and intermediate ztp_script.py
file name

<HUAWEI> system-view
[~HUAWEI] sysname dhcp_server
[*HUAWEI] commit
[~dhcp_server] dhcp enable
[~dhcp_server] dhcp server request-packet all-interface enable
[*dhcp_server] ip pool pool1
[*dhcp_server-ip-pool-pool1] gateway-list 10.1.1.1
[*dhcp_server-ip-pool-pool1] network 10.1.1.0 mask 255.255.255.0
[*dhcp_server-ip-pool-pool1] option 67 cipher sftp://sftpuser:Pwd123@10.1.3.2/ztp_script.py
[*dhcp_server-ip-pool-pool1] quit

# Configure the IP address and gateway of the DHCP server. Ensure that the DHCP
server and gateway of SwitchA and SwitchB have reachable routes to each other.

Step 5 Configure the DHCP relay agent.

# On SwitchC, configure the DHCP relay function and set the IP address of the
interface connected to SwitchA and SwitchB to 10.1.1.1. The interface functions as
the default gateway of SwitchA and SwitchB.
<HUAWEI> system-view
[~HUAWEI] sysname SwitchC
[*HUAWEI] commit
[~SwitchC] vlan batch 10
[*SwitchC] interface 10ge 1/0/1
[*SwitchC-10GE1/0/1] port link-type trunk
[*SwitchC-10GE1/0/1] port trunk allow-pass vlan 10
[*SwitchC-10GE1/0/1] port trunk pvid vlan 10
[*SwitchC-10GE1/0/1] quit
[*SwitchC] interface 10ge 1/0/2
[*SwitchC-10GE1/0/2] port link-type trunk
[*SwitchC-10GE1/0/2] port trunk allow-pass vlan 10
[*SwitchC-10GE1/0/2] port trunk pvid vlan 10
[*SwitchC-10GE1/0/2] quit
[*SwitchC] interface vlanif 10
[*SwitchC-Vlanif10] ip address 10.1.1.1 24
[*SwitchC-Vlanif10] quit
[*SwitchC] dhcp enable
[*SwitchC] interface vlanif 10
[*SwitchC-Vlanif10] dhcp select relay
[*SwitchC-Vlanif10] dhcp relay binding server ip 10.1.2.2
[*SwitchC-Vlanif10] commit

Step 6 Power on SwitchA and SwitchB to start the ZTP process.

Step 7 Verify the configuration.

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 108


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# The switches complete the ZTP process 15 minutes after they are powered on.
Log in to the stack and run the display startup command to check whether the
current system software and configuration files are the required ones.
<SwitchA> display startup
======================================================================
Chassis ID: 1
--------------------------------------------------------------------
1/5 (system master board):
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc
Next startup system software: flash:/CE12800-V200R019C10.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
--------------------------------------------------------------------
1/6 (slave board):
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc
Next startup system software: flash:/CE12800-V200R019C10.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
======================================================================

======================================================================
Chassis ID: 2
--------------------------------------------------------------------
2/5 (system slave board):
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc
Next startup system software: flash:/CE12800-V200R019C10.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
--------------------------------------------------------------------
2/6 (slave board):
Configured startup system software: flash:/CE12800-V200R019C10.cc
Startup system software: flash:/CE12800-V200R019C10.cc
Next startup system software: flash:/CE12800-V200R019C10.cc
Startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Next startup saved-configuration file: flash:/conf_210235527210D4000028.cfg
Startup paf file: default
Next startup paf file: default
Startup patch package: NULL
Next startup patch package: NULL
======================================================================

----End

ztp_script.py File and Configuration Files


ztp_script.py File

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 109


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

NOTE

#sha256sum= is the SHA256 code of the intermediate file ztp_script.py. You can modify the
contents of ztp_script.py according to actual networking requirements. After the modification,
use an SHA256 calculation tool, such as sha256sum, to generate the SHA256 code of the
modified file.
The intermediate file cannot contain #sha256sum= when the SHA256 code is generated. Add
#sha256sum= to the beginning of the script file after the SHA256 code is generated.
This script file is used for reference only and can be transferred using SFTP. You can modify the
script file based on deployment requirements.
#sha256sum="7eb57f7cf11d2bf481e544f333267e8dd43a532c12a60151d640d64872013a8b"
#!/usr/bin/env python
#
# Copyright (C) Huawei Technologies Co., Ltd. 2008-2013. All rights reserved.
# ----------------------------------------------------------------------------------------------------------------------
# History:
# Date Author Modification
# 20130629 Author created file.
# ----------------------------------------------------------------------------------------------------------------------

"""
Zero Touch Provisioning (ZTP) enables devices to automatically load version files including system software,
patch files, configuration files when the device starts up, the devices to be configured must be new devices
or have no configuration files.

This is a sample of Zero Touch Provisioning user script. You can customize it to meet the requirements of
your network environment.
"""

import http.client
import urllib.request, urllib.parse, urllib.error
import string
import re
import xml.etree.ElementTree as etree
import os
import stat
import logging
import traceback
import hashlib
import sys

from urllib.parse import urlparse


from urllib.parse import urlunparse
from time import sleep

# error code
OK =0
ERR =1

# File server in which stores the necessary system software, configuration and patch files:
# 1) Specify the file server which supports the following format.
# sftp://[username[:password]@]hostname[:port]
# 2) Do not add a trailing slash at the end of file server path.
FILE_SERVER = 'sftp://sftpuser:Pwd123@10.1.3.2'

# Remote file paths:


# 1) The path may include directory name and file name.
# 2) If file name is not specified, indicate the procedure can be skipped.
# File paths of system software on file server, filename extension is '.cc'.
REMOTE_PATH_IMAGE = {
'CE12800' : '/CE12800-V200R019C10.cc',}
# File path of configuration file on file server, filename extension is '.cfg', '.zip' or '.dat'.
REMOTE_PATH_CONFIG = '/conf_%s.cfg'
# File path of patch file on file server, filename extension is '.pat'
REMOTE_PATH_PATCH = {
'CE12800' : '',
}

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 110


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# File path of stack member ID file on file server, filename extension is '.txt'
REMOTE_PATH_MEMID = '/stack_memberid.txt'
# File path of license list file, filename extension is '.xml'
REMOTE_PATH_LICLIST = ''
# File path of sha256 file, contains sha256 value of image / patch / memid / license file, file extension is
'.txt'
REMOTE_PATH_SHA256 = ''
# File path of python file on file server, filename extension is '.py'
REMOTE_PATH_PYTHON = ''

# Max times to retry get startup when no query result


GET_STARTUP_INTERVAL = 15 # seconds
MAX_TIMES_GET_STARTUP = 120 # Max times to retry

# Max times to retry when download file faild


MAX_TIMES_RETRY_DOWNLOAD = 3

class OPSConnection(object):
"""Make an OPS connection instance."""

def __init__(self, host, port = 80):


self.host = host
self.port = port
self.headers = {
"Content-type": "application/xml",
"Accept": "application/xml"
}

self.conn = http.client.HTTPConnection(self.host, self.port)

def close(self):
"""Close the connection"""
self.conn.close()

def create(self, uri, req_data):


"""Create a resource on the server"""
ret = self._rest_call("POST", uri, req_data)
return ret

def delete(self, uri, req_data):


"""Delete a resource on the server"""
ret = self._rest_call("DELETE", uri, req_data)
return ret

def get(self, uri, req_data = None):


"""Retrieve a resource from the server"""
ret = self._rest_call("GET", uri, req_data)
return ret

def set(self, uri, req_data):


"""Update a resource on the server"""
ret = self._rest_call("PUT", uri, req_data)
return ret

def _rest_call(self, method, uri, req_data):


"""REST call"""
if req_data == None:
body = ""
else:
body = req_data

logging.info('HTTP request: %s %s HTTP/1.1', method, uri)


self.conn.request(method, uri, body, self.headers)
response = self.conn.getresponse()
ret = (response.status, response.reason, response.read())
if response.status != http.client.OK:
logging.info('%s', body)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 111


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

logging.error('HTTP response: HTTP/1.1 %s %s\n%s', ret[0], ret[1], ret[2])


return ret

class OPIExecError(Exception):
"""OPI executes error."""
pass

class ZTPErr(Exception):
"""ZTP error."""
pass

def get_addr_by_hostname(ops_conn, host, addr_type = '1'):


"""Translate a host name to IPv4 address format. The IPv4 address is returned as a string."""
logging.info("Get IP address by host name...")
uri = "/dns/dnsNameResolution"
root_elem = etree.Element('dnsNameResolution')
etree.SubElement(root_elem, 'host').text = host
etree.SubElement(root_elem, 'addrType').text = addr_type
req_data = etree.tostring(root_elem, "UTF-8")
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to get address by host name')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "ipv4Addr", namespaces)
if elem is None:
raise OPIExecError('Failed to get IP address by host name')

return elem.text

def _del_rsa_peer_key(ops_conn, key_name):


"""Delete RSA peer key configuration"""
logging.info("Delete RSA peer key %s", key_name)
uri = "/rsa/rsaPeerKeys/rsaPeerKey"
root_elem = etree.Element('rsaPeerKey')
etree.SubElement(root_elem, 'keyName').text = key_name
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete RSA peer key')

except Exception as reason:


logging.error(reason)

def _del_sshc_rsa_key(ops_conn, server_name, key_type = 'RSA'):


"""Delete SSH client RSA key configuration"""
logging.debug("Delete SSH client RSA key for %s", server_name)
uri = "/sshc/sshCliKeyCfgs/sshCliKeyCfg"
root_elem = etree.Element('sshCliKeyCfg')
etree.SubElement(root_elem, 'serverName').text = server_name
etree.SubElement(root_elem, 'pubKeyType').text = key_type
req_data = etree.tostring(root_elem, "UTF-8")
try:
ret, _, _ = ops_conn.delete(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete SSH client RSA key')

except Exception as reason:


logging.error(reason)

_del_rsa_peer_key(ops_conn, server_name)

def _set_sshc_first_time(ops_conn, switch):


"""Set SSH client attribute of authenticating user for the first time access"""

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 112


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if switch not in ['Enable', 'Disable']:


return ERR

logging.info('Set SSH client first-time enable switch = %s', switch)


uri = "/sshc/sshClient"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshClient>
<firstTimeEnable>$enable</firstTimeEnable>
</sshClient>
''')
req_data = str_temp.substitute(enable = switch)
ret, _, _ = ops_conn.set(uri, req_data)
if ret != http.client.OK:
if switch == 'Enable':
raise OPIExecError('Failed to enable SSH client first-time')
else:
raise OPIExecError('Failed to disable SSH client first-time')

return OK

def _sftp_download_file(ops_conn, url, local_path):


"""Download file using SFTP."""
_set_sshc_first_time(ops_conn, 'Enable')

logging.info('SFTP download "%s" to "%s".', url, local_path)


uri = "/sshc/sshcConnects/sshcConnect"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<sshcConnect>
<HostAddrIPv4>$serverIp</HostAddrIPv4>
<commandType>get</commandType>
<userName>$username</userName>
<password>$password</password>
<localFileName>$localPath</localFileName>
<remoteFileName>$remotePath</remoteFileName>
<identityKey>ssh-rsa</identityKey>
<transferType>SFTP</transferType>
</sshcConnect>
''')
url_tuple = urlparse(url)
if re.match(r"\d+\.\d+\.\d+\.\d+", url_tuple.hostname):
server_ip = url_tuple.hostname
else:
server_ip = get_addr_by_hostname(ops_conn, url_tuple.hostname)
req_data = str_temp.substitute(serverIp = server_ip, username = url_tuple.username, password =
url_tuple.password,
remotePath = url_tuple.path[1:], localPath = local_path)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
print(('Failed to download file "%s" using SFTP' % os.path.basename(local_path)))
sys.stdout.flush()
ret = ERR
else:
ret = OK

_del_sshc_rsa_key(ops_conn, server_ip)
_set_sshc_first_time(ops_conn, 'Disable')
return ret

def _usb_download_file(ops_conn, url, local_path):


"""Download file using usb"""
logging.info('USB download "%s" to "%s".', url, local_path)

url_tuple = urlparse(url, allow_fragments=False)


src_path = url_tuple.path[1:]
try:
copy_file(ops_conn, src_path, local_path)
except:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 113


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

print(('Failed to download file "%s" using USB' % os.path.basename(local_path)))


sys.stdout.flush()
return ERR
return OK

def download_file(ops_conn, url, local_path, retry_times = 0):


"""Download file, support SFTP.

sftp://[username[:password]@]hostname[:port]/path

Args:
ops_conn: OPS connection instance
url: URL of remote file
local_path: local path to put the file

Returns:
A integer of return code
"""
url_tuple = urlparse(url)
print(("Info: Download %s to %s" % (url_tuple.path[1:], local_path)))
sys.stdout.flush()
func_dict = {'sftp': _sftp_download_file,
'file': _usb_download_file}
scheme = url_tuple.scheme
if scheme not in list(func_dict.keys()):
raise ZTPErr('Unknown file transfer scheme %s' % scheme)

ret = OK
cnt = 0
while (cnt < 1 + retry_times):
if cnt:
print('Retry downloading...')
sys.stdout.flush()
logging.info('Retry downloading...')
ret = func_dict[scheme](ops_conn, url, local_path)
if ret is OK:
break
cnt += 1

if ret is not OK:


raise ZTPErr('Failed to download file "%s"' % os.path.basename(url))

return OK

class StartupInfo(object):
"""Startup configuration information

image: startup system software


config: startup saved-configuration file
patch: startup patch package
"""
def __init__(self, image = None, config = None, patch = None):
self.image = image
self.config = config
self.patch = patch

class Startup(object):
"""Startup configuration information

current: current startup configuration


next: current next startup configuration
"""
def __init__(self, ops_conn):
self.ops_conn = ops_conn
self.current, self.next = self._get_startup_info()

def _get_startup_info(self):
"""Get the startup information."""

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 114


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

logging.info("Get the startup information...")


uri = "/cfg/startupInfos/startupInfo"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<startupInfo>
<position/>
<configedSysSoft/>
<curSysSoft/>
<nextSysSoft/>
<curStartupFile/>
<nextStartupFile/>
<curPatchFile/>
<nextPatchFile/>
</startupInfo>'''

cnt = 0
while (cnt < MAX_TIMES_GET_STARTUP):
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
cnt += 1
logging.warning('Failed to get the startup information')
continue

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
mpath = 'data' + uri.replace('/', '/vrp:') # match path
nslen = len(namespaces['vrp'])
elem = root_elem.find(mpath, namespaces)
if elem is not None:
break
logging.warning('No query result while getting startup info')
sleep(GET_STARTUP_INTERVAL) # sleep to wait for system ready when no query result
cnt += 1

if elem is None:
raise OPIExecError('Failed to get the startup information')

current = StartupInfo() # current startup info


curnext = StartupInfo() # next startup info
for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}text'
if tag == 'curSysSoft':
current.image = child.text
elif tag == 'nextSysSoft':
curnext.image = child.text
elif tag == 'curStartupFile' and child.text != 'NULL':
current.config = child.text
elif tag == 'nextStartupFile' and child.text != 'NULL':
curnext.config = child.text
elif tag == 'curPatchFile' and child.text != 'NULL':
current.patch = child.text
elif tag == 'nextPatchFile' and child.text != 'NULL':
curnext.patch = child.text
else:
continue

return current, curnext

def _set_startup_image_file(self, file_path):


"""Set the next startup system software"""
logging.info("Set the next startup system software to %s...", file_path)
uri = "/sum/startupbymode"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startupbymode>
<softwareName>$fileName</softwareName>
<mode>STARTUP_MODE_ALL</mode>
</startupbymode>
''')
req_data = str_temp.substitute(fileName = file_path)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 115


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# it is a action operation, so use create for HTTP POST


ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup system software")

def _set_startup_config_file(self, file_path):


"""Set the next startup saved-configuration file"""
logging.info("Set the next startup saved-configuration file to %s...", file_path)
uri = "/cfg/setStartup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<setStartup>
<fileName>$fileName</fileName>
</setStartup>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup configuration file")

def _del_startup_config_file(self):
"""Delete startup config file"""
logging.info("Delete the next startup config file...")
uri = "/cfg/clearStartup"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<clearStartup>
</clearStartup>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to delete startup configuration file")

def _set_startup_patch_file(self, file_path):


"""Set the next startup patch file"""
logging.info("Set the next startup patch file to %s...", file_path)
uri = "/patch/startup"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<startup>
<packageName>$fileName</packageName>
</startup>
''')
req_data = str_temp.substitute(fileName = file_path)
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError("Failed to set startup patch file")

def _get_cur_stack_member_id(self):
"""rest api: Get current stack member id"""

logging.info("Get current stack member ID...")


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID></memberID>
</stackMemberInfo>
'''
ret, _, rsp_data = self.ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get current stack member id, rsp not ok')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "memberID", namespaces)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 116


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if elem is None:
raise OPIExecError('Failed to get the current stack member id for no "memberID" element')

return elem.text

def _set_stack_member_id(self, file_path, esn):


"""Set the next stack member ID"""

def get_stackid_from_file(fname, esn):


"""parse esn_id.txt file and get stack id according to esn num
format of esn_stackid file is like below:

sn Irf group Irf number


Sdddg 100 1
Sddde 100 2
"""
# fname must exist, guaranteed by caller
fname = os.path.basename(fname)
with open(fname, 'r') as item:
for line in item:
token = line.strip('[\r\n]')
token = token.split()
if token[0] == esn:
return token[2]
return None

logging.info('Set the next stack member ID, filename %s', file_path)


uri = "/stack/stackMemberInfos/stackMemberInfo"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>$curmemberid</memberID>
<nextMemberID>$memberid</nextMemberID>
</stackMemberInfo>
''')

cur_memid = self._get_cur_stack_member_id()
next_memid = get_stackid_from_file(file_path, esn)
if not next_memid:
logging.error('Failed to get stack id from %s, esn %s', file_path, esn)
return

req_data = str_temp.substitute(curmemberid = cur_memid, memberid = next_memid)


ret, _, _ = self.ops_conn.set(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to set stack id {}'.format(next_memid))

return OK

def _reset_stack_member_id(self):
"""rest api: reset stack member id"""

logging.info('Reset the next stack member ID')


uri = "/stack/stackMemberInfos/stackMemberInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<stackMemberInfo>
<memberID>1</memberID>
<nextMemberID>1</nextMemberID>
</stackMemberInfo>
'''

ret, _, _ = self.ops_conn.set(uri, req_data)


if ret != http.client.OK:
raise OPIExecError('Failed to reset stack id ')

return OK

def _reset_startup_patch_file(self):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 117


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

"""Rest patch file for system to startup"""


logging.info("Reset the next startup patch file...")
uri = "/patch/resetpatch"
req_data = '''<?xml version="1.0" encoding="UTF-8"?>
<resetpatch>
</resetpatch>
'''
# it is a action operation, so use create for HTTP POST
ret, _, _ = self.ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to reset patch')

def reset_startup_info(self, slave):


"""Reset startup info and delete the downloaded files"""
logging.info("Reset the next startup information...")
_, configured = self._get_startup_info()

# 1. Reset next startup config file and delete it


try:
if configured.config != self.next.config:
if self.next.config is None:
self._del_startup_config_file()
else:
self._set_startup_config_file(self.next.config)
if configured.config is not None:
del_file_all(self.ops_conn, configured.config, slave)

except Exception as reason:


logging.error(reason)

# 2. Reset next startup patch file


try:
if configured.patch != self.next.patch:
if self.next.patch is None:
self._reset_startup_patch_file()
else:
self._set_startup_patch_file(self.next.patch)

if configured.patch is not None:


del_file_all(self.ops_conn, configured.patch, slave)
except Exception as reason:
logging.error(reason)

# 3. Reset next startup system software and delete it


try:
if configured.image != self.next.image:
self._set_startup_image_file(self.next.image)
del_file_all(self.ops_conn, configured.image, slave)
except Exception as reason:
logging.error(reason)

# 4. reset stack member id


try:
self._reset_stack_member_id()
except Exception as reason:
logging.error(reason)

def set_startup_info(self, image_file, config_file, patch_file, memid_file, slave, esn_str):


"""Set the next startup information."""
logging.info("Set the next startup information...")
# 1. Set next startup system software
if image_file is not None:
try:
self._set_startup_image_file(image_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, image_file, slave)
self.reset_startup_info(slave)
raise

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 118


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

# 2. Set next startup config file


if config_file is not None:
try:
self._set_startup_config_file(config_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, config_file, slave)
self.reset_startup_info(slave)
raise

# 3. Set next startup patch file


if patch_file is not None:
try:
self._set_startup_patch_file(patch_file)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, patch_file, slave)
self.reset_startup_info(slave)
raise

# 4. Set next member id


if memid_file is not None:
try:
self._set_stack_member_id(memid_file, esn_str)
except Exception as reason:
logging.error(reason)
del_file_all(self.ops_conn, memid_file, None)
self.reset_startup_info(slave)
raise

def get_cwd(ops_conn):
"""Get the full filename of the current working directory"""
logging.info("Get the current working directory...")
uri = "/vfm/pwds/pwd"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<pwd>
<dictionaryName/>
</pwd>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the current working directory')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "dictionaryName", namespaces)
if elem is None:
raise OPIExecError('Failed to get the current working directory for no "directoryName" element')

return elem.text

def file_exist(ops_conn, file_path):


"""Returns True if file_path refers to an existing file, otherwise returns False"""
uri = "/vfm/dirs/dir"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<dir>
<fileName>$fileName</fileName>
</dir>
''')
req_data = str_temp.substitute(fileName = file_path)
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to list information about the file "%s"' % file_path)

root_elem = etree.fromstring(rsp_data)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 119


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}


uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
elem = root_elem.find(uri + "fileName", namespaces)
if elem is None:
return False

return True

def del_file(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>
</deleteFileUnRes>
''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to delete the file "%s" permanently' % file_path)

except Exception as reason:


logging.error(reason)

def del_file_noerror(ops_conn, file_path):


"""Delete a file permanently"""
if file_path is None or file_path == '':
return

logging.info("Delete file %s permanently", file_path)


uri = "/vfm/deleteFileUnRes"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<deleteFileUnRes>
<fileName>$filePath</fileName>
</deleteFileUnRes>
''')
req_data = str_temp.substitute(filePath = file_path)
try:
# it is a action operation, so use create for HTTP POST
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.info("file not exist")

except Exception as reason:


logging.error(reason)

def del_file_all(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file(ops_conn, file_path)
if slave:
del_file(ops_conn, 'slave#' + file_path)

def del_file_all_noerror(ops_conn, file_path, slave):


"""Delete a file permanently on all main boards"""
if file_path:
del_file_noerror(ops_conn, file_path)
if slave:
del_file_noerror(ops_conn, 'slave#' + file_path)

def copy_file(ops_conn, src_path, dest_path):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 120


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

"""Copy a file"""
print(('Info: Copy file %s to %s...' % (src_path, dest_path)))
sys.stdout.flush()
logging.info('Copy file %s to %s...', src_path, dest_path)
uri = "/vfm/copyFile"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<copyFile>
<srcFileName>$src</srcFileName>
<desFileName>$dest</desFileName>
</copyFile>
''')
req_data = str_temp.substitute(src = src_path, dest = dest_path)

# it is a action operation, so use create for HTTP POST


ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
raise OPIExecError('Failed to copy "%s" to "%s"' % (src_path, dest_path))

def has_slave_mpu(ops_conn):
"""Whether device has slave MPU, returns a bool value"""
logging.info("Test whether device has slave MPU...")
uri = "/devm/phyEntitys"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<phyEntitys>
<phyEntity>
<entClass>mpuModule</entClass>
<entStandbyState/>
<position/>
</phyEntity>
</phyEntitys>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the device slave information')

root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:') + '/vrp:'
for entity in root_elem.findall(uri + 'phyEntity', namespaces):
elem = entity.find("vrp:entStandbyState", namespaces)
if elem is not None and elem.text == 'slave':
return True

return False

def get_system_info(ops_conn):
"""Get system info, returns a dict"""
logging.info("Get the system information...")
uri = "/system/systemInfo"
req_data = \
'''<?xml version="1.0" encoding="UTF-8"?>
<systemInfo>
<productName/>
<esn/>
<mac/>
</systemInfo>
'''
ret, _, rsp_data = ops_conn.get(uri, req_data)
if ret != http.client.OK or rsp_data == '':
raise OPIExecError('Failed to get the system information')

sys_info = {}.fromkeys(('productName', 'esn', 'mac'))


root_elem = etree.fromstring(rsp_data)
namespaces = {'vrp' : 'http://www.huawei.com/netconf/vrp'}
uri = 'data' + uri.replace('/', '/vrp:')
nslen = len(namespaces['vrp'])
elem = root_elem.find(uri, namespaces)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 121


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

if elem is not None:


for child in elem:
tag = child.tag[nslen + 2:] # skip the namespace, '{namespace}esn'
if tag in list(sys_info.keys()):
sys_info[tag] = child.text

return sys_info

def test_file_paths(image, config, patch, stack_memid, sha256_file, license_list_file):


"""Test whether argument paths are valid."""
logging.info("Test whether argument paths are valid...")
# check image file path
file_name = os.path.basename(image)
if file_name != '' and not file_name.lower().endswith('.cc'):
print('Error: Invalid filename extension of system software')
sys.stdout.flush()
return False

# check config file path


file_name = os.path.basename(config)
file_name = file_name.lower()
_, ext = os.path.splitext(file_name)
if file_name != '' and ext not in ['.cfg', '.zip', '.dat']:
print('Error: Invalid filename extension of configuration file')
sys.stdout.flush()
return False

# check patch file path


file_name = os.path.basename(patch)
if file_name != '' and not file_name.lower().endswith('.pat'):
print('Error: Invalid filename extension of patch file')
sys.stdout.flush()
return False

# check stack member id file path


file_name = os.path.basename(stack_memid)
if file_name != '' and not file_name.lower().endswith('.txt'):
print('Error: Invalid filename extension of stack member ID file')
sys.stdout.flush()
return False

# check sha256 file path


file_name = os.path.basename(sha256_file)
if file_name != '' and not file_name.lower().endswith('.txt'):
print('Error: Invalid filename extension of sha256 file')
sys.stdout.flush()
return False

# check license list file path


file_name = os.path.basename(license_list_file)
if file_name != '' and not file_name.lower().endswith('.xml'):
print('Error: Invalid filename extension of license list file')
sys.stdout.flush()
return False

return True

def sha256sum(fname, need_skip_first_line = False):


"""
Calculate sha256 num for this file.
"""

def read_chunks(fhdl):
'''read chunks'''
chunk = fhdl.read(8096)
while chunk:
yield chunk
chunk = fhdl.read(8096)
else:

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 122


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

fhdl.seek(0)

sha256_obj = hashlib.sha256()
if isinstance(fname, str) and os.path.exists(fname):
with open(fname, "rb") as fhdl:
#skip the first line
fhdl.seek(0)
if need_skip_first_line:
fhdl.readline()
for chunk in read_chunks(fhdl):
sha256_obj.update(chunk)
elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file):
for chunk in read_chunks(fname):
sha256_obj.update(chunk)
else:
pass
return sha256_obj.hexdigest()

def sha256_get_from_file(fname):
"""Get sha256 num form file, stored in first line"""

with open(fname, "r") as fhdl:


fhdl.seek(0)
line_first = fhdl.readline()

# if not match pattern, the format of this file is not supported


if not re.match('^#sha256sum="[\\w]{64}"[\r\n]+$', line_first):
return 'None'

return line_first[12:76]

def sha256_check_with_first_line(fname):
"""Validate sha256 for this file"""

fname = os.path.basename(fname)
sha256_calc = sha256sum(fname, True)
sha256_file = sha256_get_from_file(fname)

if sha256_file.lower() != sha256_calc:
logging.warning('sha256 check failed, file %s', fname)
print(('sha256 checksum of the file "%s" is %s' % (fname, sha256_calc)))
sys.stdout.flush()
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256_calc)
print(('sha256 checksum received from the file "%s" is %s' % (fname, sha256_file)))
sys.stdout.flush()
logging.warning('sha256 checksum received from the file "%s" is %s', fname, sha256_file)
return False

return True

def sha256_check_with_dic(sha256_dic, fname):


"""sha256 check with dic"""
if fname not in sha256_dic:
logging.info('sha256_dic does not has key %s, no need to do sha256 verification', fname)
return True

sha256sum_result = sha256sum(fname, False)


if sha256_dic[fname] == sha256sum_result:
return True

print(('sha256 checksum of the file "%s" is %s' % (fname, sha256sum_result)))


sys.stdout.flush()
print(('sha256 checksum received for the file "%s" is %s' % (fname, sha256_dic[fname])))
sys.stdout.flush()
logging.warning('sha256 check failed, file %s', fname)
logging.warning('sha256 checksum of the file "%s" is %s', fname, sha256sum_result)
logging.warning('sha256 checksum received for the file "%s" is %s', fname, sha256_dic[fname])

return False

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 123


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

def parse_sha256_file(fname):
"""parse sha256 file"""

def read_line(fhdl):
"""read a line by loop"""
line = fhdl.readline()
while line:
yield line
line = fhdl.readline()
else:
fhdl.seek(0)

sha256_dic = {}
with open(fname, "rb") as fhdl:
for line in read_line(fhdl):
line_spilt = line.split()
if 2 != len(line_spilt):
continue
dic_tmp = {line_spilt[0]: line_spilt[1]}
sha256_dic.update(dic_tmp)
return sha256_dic

def verify_and_parse_sha256_file(fname):
"""
vefiry data integrity of sha256 file and parse this file

format of this file is like:


------------------------------------------------------------------
#sha256sum="517cf194e2e1960429c6aedc0e4dba37"

file-name sha256
conf_5618642831132.cfg c0ace0f0542950beaacb39cd1c3b5716
------------------------------------------------------------------
"""
if not sha256_check_with_first_line(fname):
return ERR, None
return OK, parse_sha256_file(fname)

def check_parameter(aset):
seq = ['&', '>', '<', '"', "'"]
if aset:
for c in seq:
if c in aset:
return True
return False

def check_filename(ops_conn):
sys_info = get_system_info(ops_conn)
url_tuple = urlparse(FILE_SERVER)
if check_parameter(url_tuple.username) or check_parameter(url_tuple.password):
raise ZTPErr('Invalid username or password, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of system software, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_CONFIG)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of configuration file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_PATCH.get(sys_info['productName'], ''))
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of patch file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_MEMID)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of member ID file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_SHA256)
if file_name != '' and check_parameter(file_name):
raise ZTPErr('Invalid filename of sha256 file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
file_name = os.path.basename(REMOTE_PATH_LICLIST)
if file_name != '' and check_parameter(file_name):

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 124


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

raise ZTPErr('Invalid filename of license list file, the name should not contain: '+'&'+' >'+' <'+' "'+" '.")
return OK

def active_license(ops_conn, license_name):


if license_name:
uri = "/lcs/lcsActive"
str_temp = string.Template(
'''<?xml version="1.0" encoding="UTF-8"?>
<lcsActive>
<lcsFileName>$lcsFileName</lcsFileName>
</lcsActive>
''')
req_data = str_temp.substitute(lcsFileName = license_name)
ret, _, _ = ops_conn.create(uri, req_data)
if ret != http.client.OK:
logging.error('Error: Failed to active license.')
return ERR
return OK

def main_proc(ops_conn):
"""Main processing"""
sys_info = get_system_info(ops_conn) # Get system info, such as esn and system mac
cwd = get_cwd(ops_conn) # Get the current working directory
startup = Startup(ops_conn)
slave = has_slave_mpu(ops_conn) # Check whether slave MPU board exists or not
chg_flag = False

check_filename(ops_conn)

# check remote file paths


if not test_file_paths(REMOTE_PATH_IMAGE.get(sys_info['productName'], ''), REMOTE_PATH_CONFIG,
REMOTE_PATH_PATCH.get(sys_info['productName'], ''), REMOTE_PATH_MEMID,
REMOTE_PATH_SHA256,
REMOTE_PATH_LICLIST):
return ERR

# download sha256 file first, used to verify data integrity of files which will be downloaded next
local_path_sha256 = None
file_path = REMOTE_PATH_SHA256
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_sha256 = cwd + file_name
ret = download_file(ops_conn, url, local_path_sha256, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download sha256 file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download sha256 file successfully')
sys.stdout.flush()
ret, sha256_dic = verify_and_parse_sha256_file(file_name)
# delete the file immediately
del_file_all(ops_conn, local_path_sha256, None)
if ret is ERR:
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
return ERR
else:
sha256_dic = {}

# download configuration file


local_path_config = None
file_path = REMOTE_PATH_CONFIG
if "%s" in file_path:
file_path = REMOTE_PATH_CONFIG % sys_info['esn']
if not file_path.startswith('/'):
file_path = '/' + file_path

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 125


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_config = cwd + file_name
del_file_noerror(ops_conn, local_path_config)
ret = download_file(ops_conn, url, local_path_config, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download configuration file "%s"' % file_name))
sys.stdout.flush()
return ERR
print('Info: Download configuration file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_config)
copy_file(ops_conn, local_path_config, 'slave#' + local_path_config)
chg_flag = True

# download patch file


local_path_patch = None
file_path = REMOTE_PATH_PATCH.get(sys_info['productName'], '')
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if startup.current.patch:
cur_pat = os.path.basename(startup.current.patch).lower()
else:
cur_pat = ''
if file_name != '' and file_name.lower() != cur_pat:
url = FILE_SERVER + file_path
local_path_patch = cwd + file_name
del_file_noerror(ops_conn, local_path_patch)
ret = download_file(ops_conn, url, local_path_patch, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download patch file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
return ERR
print('Info: Download patch file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_patch)
copy_file(ops_conn, local_path_patch, 'slave#' + local_path_patch)
chg_flag = True

# download stack member ID file


local_path_memid = None
file_path = REMOTE_PATH_MEMID
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_memid = cwd + file_name
del_file_noerror(ops_conn,local_path_memid)
ret = download_file(ops_conn, url, local_path_memid, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 126


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

del_file_all(ops_conn, local_path_config, slave)


del_file_all(ops_conn, local_path_patch, slave)
return ERR
print('Info: Download stack member ID file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
chg_flag = True
#no need copy to slave board

# download system software


local_path_image = None
file_path = REMOTE_PATH_IMAGE.get(sys_info['productName'], '')
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if startup.current.image:
cur_image = os.path.basename(startup.current.image).lower()
else:
cur_image = ''
if file_name != '' and file_name.lower() != cur_image:
url = FILE_SERVER + file_path
local_path_image = cwd + file_name
del_file_noerror(ops_conn, local_path_image)
ret = download_file(ops_conn, url, local_path_image, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
if file_exist(ops_conn, file_name):
del_file_all(ops_conn, local_path_image, slave)
print(('Error: Failed to download system software "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
return ERR
print('Info: Download system software file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_image)
copy_file(ops_conn, local_path_image, 'slave#' + local_path_image)
chg_flag = True
# download license list file
local_path_liclist = None
file_path = REMOTE_PATH_LICLIST
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
download_space = os.path.dirname(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_liclist = cwd + file_name
del_file_noerror(ops_conn, local_path_liclist)
ret = download_file(ops_conn, url, local_path_liclist, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license list file "%s"' % file_name))
sys.stdout.flush()
return ERR

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 127


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

print('Info: Download license list file successfully')


sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
chg_flag = True

#execute license list file to get license file name which end with .dat
license_name = None
if local_path_liclist is not None:
tree = etree.parse(file_name)
root = tree.getroot()
for child in root.findall('Lic'):
name = child.get('name')
esn = child.find('Esn').text
if sys_info['esn'] in esn:
license_name = name
print(('Info: License file name is "%s"' % license_name))
sys.stdout.flush()
break
if license_name == None :
print('Warning: Esn of this device is not in the license list file')
sys.stdout.flush()
#del_file_all(ops_conn, local_path_config, slave)
#del_file_all(ops_conn, local_path_patch, slave)
#del_file_all(ops_conn, local_path_memid, slave)
#del_file_all(ops_conn, local_path_image, slave)
#del_file_all(ops_conn, local_path_liclist, slave)
#return ERR

# download license file


local_path_license = None
file_path = license_name
if file_path is not None:
if not file_path.startswith('/'):
file_path = '/' + file_path
file_path = download_space + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_license = cwd + file_name
ret = download_file(ops_conn, url, local_path_license, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download license file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
return ERR
print('Info: Download license file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 128


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

chg_flag = True
#no need copy to slave board

# download python file


local_path_python = None
file_path = REMOTE_PATH_PYTHON
if not file_path.startswith('/'):
file_path = '/' + file_path
file_name = os.path.basename(file_path)
if file_name != '':
url = FILE_SERVER + file_path
local_path_python = cwd + file_name
del_file_noerror(ops_conn, local_path_python)
ret = download_file(ops_conn, url, local_path_python, MAX_TIMES_RETRY_DOWNLOAD)
if ret is ERR or not file_exist(ops_conn, file_name):
print(('Error: Failed to download python file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
return ERR
print('Info: Download python file successfully')
sys.stdout.flush()
if not sha256_check_with_dic(sha256_dic, file_name):
print(('Error: sha256 check failed, file "%s"' % file_name))
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
if slave:
del_file_noerror(ops_conn, 'slave#' + local_path_python)
copy_file(ops_conn, local_path_python, 'slave#' + local_path_python)
chg_flag = True

if chg_flag is False:
return ERR

# active license file


if local_path_license is not None:
ret = active_license(ops_conn, local_path_license)
if ret is ERR:
print('Info: Active license failed')
sys.stdout.flush()
del_file_all(ops_conn, local_path_config, slave)
del_file_all(ops_conn, local_path_patch, slave)
del_file_all(ops_conn, local_path_memid, slave)
del_file_all(ops_conn, local_path_image, slave)
del_file_all(ops_conn, local_path_liclist, slave)
del_file_all(ops_conn, local_path_license, slave)
del_file_all(ops_conn, local_path_python, slave)
return ERR
print(('Info: Active license sucessfully, name: %s' % local_path_license))
sys.stdout.flush()
sleep(10)
# set startup info
startup.set_startup_info(local_path_image, local_path_config, local_path_patch,
local_path_memid, slave, sys_info['esn'])

# delete stack member ID file and license list file after used
del_file_all(ops_conn, local_path_memid, None)
del_file_all(ops_conn, local_path_liclist, None)

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 129


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

return OK

def main(usb_path = ''):


"""The main function of user script. It is called by ZTP frame, so do not remove or change this function.

Args:
Raises:
Returns: user script processing result
"""
host = "localhost"
if usb_path and len(usb_path):
logging.info('ztp_script usb_path: %s', usb_path)
global FILE_SERVER
FILE_SERVER = 'file:///' + usb_path
try:
# Make an OPS connection instance.
ops_conn = OPSConnection(host)
ret = main_proc(ops_conn)

except OPIExecError as reason:


logging.error('OPI execute error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except ZTPErr as reason:


logging.error('ZTP error: %s', reason)
print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except IOError as reason:


print(("Error: %s" % reason))
sys.stdout.flush()
ret = ERR

except Exception as reason:


logging.error(reason)
traceinfo = traceback.format_exc()
logging.debug(traceinfo)
ret = ERR

finally:
# Close the OPS connection
ops_conn.close()

return ret

if __name__ == "__main__":
main()

Configuration Files

● Configuration file of SwitchC


#
sysname SwitchC
#
vlan batch 10
#
dhcp enable
#
interface Vlanif10
ip address 10.1.1.1 255.255.255.0
dhcp select relay
dhcp relay binding server ip 10.1.2.2
#
interface 10GE1/0/1
port link-type trunk

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 130


CloudEngine 12800 Series Switches
Configuration Guide - Basic Configuration 2 ZTP Configuration

port trunk pvid vlan 10


port trunk allow-pass vlan 10
#
interface 10GE1/0/2
port link-type trunk
port trunk pvid vlan 10
port trunk allow-pass vlan 10
#
return

Issue 02 (2021-09-30) Copyright © Huawei Technologies Co., Ltd. 131

You might also like