3 ############################################################################
4 # Copyright 2016 RIFT.io Inc #
6 # Licensed under the Apache License, Version 2.0 (the "License"); #
7 # you may not use this file except in compliance with the License. #
8 # You may obtain a copy of the License at #
10 # http://www.apache.org/licenses/LICENSE-2.0 #
12 # Unless required by applicable law or agreed to in writing, software #
13 # distributed under the License is distributed on an "AS IS" BASIS, #
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15 # See the License for the specific language governing permissions and #
16 # limitations under the License. #
17 ############################################################################
20 from contextlib import closing
31 class OnboardPkgError(Exception):
35 class OnboardPkgInputError(OnboardPkgError):
39 class OnboardPkgMissingPkg(OnboardPkgError):
43 class OnboardPkgFileError(OnboardPkgError):
47 class OnboardPkgMissingDescId(OnboardPkgError):
51 class OnboardPkgInvalidDescId(OnboardPkgError):
55 class OnboardPkgMissingAcct(OnboardPkgError):
59 class OnboardPkgSoConnError(OnboardPkgError):
63 class OnboardPkgCmdError(OnboardPkgError):
67 class OnboardPkgUploadError(OnboardPkgError):
71 class OnboardPkgRcConnError(OnboardPkgError):
75 class OnboardPkgDcError(OnboardPkgError):
79 class OnboardPkgAcctError(OnboardPkgError):
83 class OnboardPkgNsdError(OnboardPkgError):
87 class OnboardPkgInstError(OnboardPkgError):
91 class OnboardPkgInvalidPort(OnboardPkgError):
105 self._service_name = None
110 self._ip = args.so_ip
112 self._uport = args.upload_port
113 self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
117 self._rport = args.restconf_port
118 self._user = args.restconf_user
119 self._password = args.restconf_password
120 self._headers = '-H "accept: application/json"' + \
121 ' -H "content-type: application/json"'
122 self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config". \
123 format(header=self._headers,
125 passwd=self._password,
128 self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational". \
129 format(header=self._headers,
131 passwd=self._password,
139 def validate_args(self):
140 if args.upload_pkg is not None:
141 self._pkgs = args.upload_pkg
142 self.log.debug("Packages to upload: {}".format(self._pkgs))
143 if len(self._pkgs) == 0:
144 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
146 for pkg in self._pkgs:
147 self.log.debug("Check pkg: {}".format(pkg))
148 if os.path.isfile(pkg) is False:
149 raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
152 if args.nsd_id is None:
153 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
157 uuid.UUID(args.datacenter)
158 self._dc = args.datacenter
159 except ValueError as e:
160 raise OnboardPkgInvalidDescId("Invalid UUID for datacenter: {}".
161 format(args.datacenter))
163 elif args.vim_account:
164 self._account = args.vim_account
167 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
169 self._service_name = args.instantiate
170 self._nsd_id = args.nsd_id
172 self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
176 if (self._pkgs is None) and (self._nsd_id is None):
177 raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate options")
179 # Validate the port numbers are correct
180 def valid_port(port):
181 if 1 <= port <= 65535:
185 if not valid_port(self._uport):
186 raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
188 if not valid_port(self._rport):
189 raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
191 def _exec_cmd(self, cmd):
192 self.log.debug("Execute command: {}".format(cmd))
193 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
194 stderr=subprocess.PIPE, shell=True)
195 (output, err) = proc.communicate()
197 self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
198 format(rc, output, err))
200 raise OnboardPkgCmdError("Command {} failed ({}): {}".
201 format(cmd, rc, err))
202 return output.decode("utf-8")
204 def validate_connectivity(self):
206 self.log.debug("Check connectivity to SO at {}:{}".
207 format(self._ip, self._uport))
209 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
210 if sock.connect_ex((self._ip, self._uport)) != 0:
211 raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
212 format(self._ip, self._uport))
213 self.log.debug("Connection to SO upload port succeeded")
216 self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
217 format(self._ip, self._rport, self._user, self._password))
219 rest_url = self._conf_url+"/resource-orchestrator"
221 output = self._exec_cmd(rest_url)
222 self.log.debug("Output of restconf validation: {}".
225 js = json.loads(output)
227 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
230 self.log.debug("Connection to SO restconf port succeeded")
232 except OnboardPkgCmdError as e:
233 self.log.error("SO restconf connect failed: {}".format(e))
234 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
238 def _upload_package(self, pkg):
239 upload_cmd = "{url} -F \"descriptor=@{pkg}\" ". \
240 format(url=self._upload_url,
242 self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
244 output = self._exec_cmd(upload_cmd)
246 # Get the transaction id and wait for upload to complete
247 tx_id = json.loads(output)['transaction_id']
249 upload_status_url = "{url}/{id}/state". \
250 format(url=self._upload_url,
253 while status not in ['success', 'failure']:
254 output = self._exec_cmd(upload_status_url)
255 js = json.loads(output)
256 self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
257 status = js['status']
259 if status != 'success':
260 raise OnboardPkgUploadError("Package {} upload failed: {}".
261 format(pkg, js['errors']))
263 self.log.info("Upload of package {} succeeded".format(pkg))
265 def upload_packages(self):
266 if self._pkgs is None:
267 self.log.debug("Upload packages not provided")
270 for pkg in self._pkgs:
271 self._upload_package(pkg)
273 def instantiate(self):
274 if self._nsd_id is None:
275 self.log.debug("No NSD ID provided for instantiation")
278 # Check to see if datacenter is valid
280 dc_url = "{url}/datacenters". format(url=self._oper_url)
281 output = self._exec_cmd(dc_url)
282 if (output is None) or (len(output) == 0):
284 raise OnboardPkgDcError("Datacenter {} provided is not valid".
287 js = json.loads(output)
288 if "ro-accounts" in js["rw-launchpad:datacenters"]:
289 for ro in js["rw-launchpad:datacenters"]["ro-accounts"]:
290 if "datacenters" in ro:
291 for dc in ro["datacenters"]:
292 if dc["uuid"] == self._dc:
293 self.log.debug("Found datacenter {}".format(dc))
300 raise OnboardPkgDcError("Datacenter {} provided is not valid".
304 # Check cloud account is valid, if provided
306 acct_url = "{url}/cloud/account/{acct}". \
307 format(url=self._conf_url, acct=self._account)
308 output = self._exec_cmd(acct_url)
309 if (output is None) or (len(output) == 0):
311 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
312 format(self._account))
314 # Check id NSD ID is valid
315 nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
316 format(url=self._conf_url, nsd_id=self._nsd_id)
317 output = self._exec_cmd(nsd_url)
318 if (output is None) or (len(output) == 0):
320 raise OnboardPkgNsdError("NSD ID {} provided is not valid".
321 format(self._nsd_id))
323 js = json.loads(output)
325 raise OnboardPkgNsdError("NSD ID {} error: {}".
330 self.log.debug("NSD to instantiate: {}".format(nsd))
332 # Generate a UUID for NS
333 ns_id = str(uuid.uuid4())
334 self.log.debug("NS instance uuid: {}".format(ns_id))
336 # Build the nsr post data
338 'name': self._service_name,
341 nsr['om-datacenter'] = self._dc
343 nsr['cloud-account'] = self._account
345 data = {'nsr': [nsr]}
347 data_str = json.dumps(data)
348 self.log.debug("NSR post data: {}".format(data_str))
350 inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
351 format(url=self._conf_url, data=data_str)
352 output = self._exec_cmd(inst_url)
353 self.log.debug("Instantiate output: {}".format(output))
355 js = json.loads(output)
357 if "last-error" in js:
358 msg = "Error instantiating NS as {} with NSD {}: ". \
359 format(self._service_name, self._nsd_id,
362 raise OnboardPkgInstError(msg)
364 elif "rpc-reply" in js:
365 reply = js["rpc-reply"]
366 if "rpc-error" in reply:
367 msg = "Error instantiating NS as {} with NSD {}: ". \
368 format(self._service_name, self._nsd_id,
371 raise OnboardPkgInstError(msg)
373 self.log.info("Successfully initiated instantiation of NS as {} ({})".
374 format(self._service_name, ns_id))
378 self.validate_connectivity()
379 self.upload_packages()
383 if __name__ == "__main__":
384 parser = argparse.ArgumentParser(description='Upload and instantiate NS')
385 parser.add_argument("-s", "--so-ip", default='localhost',
386 help="SO Launchpad IP")
388 parser.add_argument("-u", "--upload-pkg", action='append',
389 help="Descriptor packages to upload. " + \
390 "If multiple descriptors are provided, they are uploaded in the same sequence.")
392 parser.add_argument("-i", "--instantiate",
393 help="Instantiate a network service with the name")
394 parser.add_argument("-d", "--nsd-id",
395 help="Network descriptor ID to instantiate")
396 parser.add_argument("-D", "--datacenter",
397 help="OpenMano datacenter to instantiate on")
398 parser.add_argument("-c", "--vim-account",
399 help="Cloud/VIM account to instantiate on")
401 parser.add_argument("-p", "--upload-port", default=4567, type=int,
402 help="Upload port number, default 4567")
403 parser.add_argument("-P", "--restconf-port", default=8008, type=int,
404 help="RESTconf port number, default 8008")
405 parser.add_argument("--restconf-user", default='admin',
406 help="RESTconf user name, default admin")
407 parser.add_argument("--restconf-password", default='admin',
408 help="RESTconf password, default admin")
410 parser.add_argument("-v", "--verbose", action='store_true',
411 help="Show more logs")
413 args = parser.parse_args()
415 fmt = logging.Formatter(
416 '%(asctime)-23s %(levelname)-5s (%(name)s@%(process)d:' \
417 '%(filename)s:%(lineno)d) - %(message)s')
418 stderr_handler = logging.StreamHandler(stream=sys.stderr)
419 stderr_handler.setFormatter(fmt)
420 logging.basicConfig(level=logging.INFO)
421 log = logging.getLogger('onboard-pkg')
422 log.addHandler(stderr_handler)
424 log.setLevel(logging.DEBUG)
426 log.debug("Input arguments: {}".format(args))
428 ob = OnboardPackage(log, args)