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 OnboardPkgAcctError(OnboardPkgError):
79 class OnboardPkgNsdError(OnboardPkgError):
83 class OnboardPkgInstError(OnboardPkgError):
87 class OnboardPkgInvalidPort(OnboardPkgError):
101 self._service_name = None
106 self._ip = args.so_ip
108 self._uport = args.upload_port
109 self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
113 self._rport = args.restconf_port
114 self._user = args.restconf_user
115 self._password = args.restconf_password
116 self._headers = '-H "accept: application/json"' + \
117 ' -H "content-type: application/json"'
118 self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config". \
119 format(header=self._headers,
121 passwd=self._password,
129 def validate_args(self):
130 if args.upload_pkg is not None:
131 self._pkgs = args.upload_pkg
132 self.log.debug("Packages to upload: {}".format(self._pkgs))
133 if len(self._pkgs) == 0:
134 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
136 for pkg in self._pkgs:
137 self.log.debug("Check pkg: {}".format(pkg))
138 if os.path.isfile(pkg) is False:
139 raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
142 if args.nsd_id is None:
143 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
147 uuid.UUID(args.datacenter)
148 self._dc = args.datacenter
149 except ValueError as e:
150 raise OnboardPkgInvalidDescId("Invalid UUID for datacenter: {}".
151 format(args.datacenter))
153 elif args.vim_account:
154 self._account = args.vim_account
157 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
159 self._service_name = args.instantiate
160 self._nsd_id = args.nsd_id
162 self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
166 if (self._pkgs is None) and (self._nsd_id is None):
167 raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate options")
169 # Validate the port numbers are correct
170 def valid_port(port):
171 if 1 <= port <= 65535:
175 if not valid_port(self._uport):
176 raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
178 if not valid_port(self._rport):
179 raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
181 def _exec_cmd(self, cmd):
182 self.log.debug("Execute command: {}".format(cmd))
183 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
184 (output, err) = proc.communicate()
186 self.log.debug("Command exec status: {}, {}, {}".format(rc, output, err))
188 raise OnboardPkgCmdError("Command {} failed ({}): {}".
189 format(cmd, rc, err))
190 return output.decode("utf-8")
192 def validate_connectivity(self):
194 self.log.debug("Check connectivity to SO at {}:{}".
195 format(self._ip, self._uport))
197 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
198 if sock.connect_ex((self._ip, self._uport)) != 0:
199 raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
200 format(self._ip, self._uport))
201 self.log.debug("Connection to SO upload port succeeded")
204 self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
205 format(self._ip, self._rport, self._user, self._password))
207 rest_url = self._conf_url+"/resource-orchestrator"
209 output = self._exec_cmd(rest_url)
210 self.log.debug("Output of restconf validation: {}".
213 js = json.loads(output)
215 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
218 self.log.debug("Connection to SO restconf port succeeded")
220 except OnboardPkgCmdError as e:
221 self.log.error("SO restconf connect failed: {}".format(e))
222 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
226 def _upload_package(self, pkg):
227 upload_cmd = "{url} -F \"descriptor=@{pkg}\" ". \
228 format(url=self._upload_url,
230 self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
232 output = self._exec_cmd(upload_cmd)
234 # Get the transaction id and wait for upload to complete
235 tx_id = json.loads(output)['transaction_id']
237 upload_status_url = "{url}/{id}/state". \
238 format(url=self._upload_url,
241 while status not in ['success', 'failure']:
242 output = self._exec_cmd(upload_status_url)
243 js = json.loads(output)
244 self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
245 status = js['status']
247 if status != 'success':
248 raise OnboardPkgUploadError("Package {} upload failed: {}".
249 format(pkg, js['errors']))
251 self.log.info("Upload of package {} succeeded".format(pkg))
253 def upload_packages(self):
254 if self._pkgs is None:
255 self.log.debug("Upload packages not provided")
258 for pkg in self._pkgs:
259 self._upload_package(pkg)
261 def instantiate(self):
262 if self._nsd_id is None:
263 self.log.debug("No NSD ID provided for instantiation")
266 # TODO: Add check to see if datacenter is valid
268 # Check cloud account is valid, if provided
270 acct_url = "{url}/cloud/account/{acct}". \
271 format(url=self._conf_url, acct=self._account)
272 output = self._exec_cmd(acct_url)
273 if (output is None) or (len(output) == 0):
275 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
276 format(self._account))
278 # Check id NSD ID is valid
279 nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
280 format(url=self._conf_url, nsd_id=self._nsd_id)
281 output = self._exec_cmd(nsd_url)
282 if (output is None) or (len(output) == 0):
284 raise OnboardPkgNsdError("NSD ID {} provided is not valid".
285 format(self._nsd_id))
287 js = json.loads(output)
289 raise OnboardPkgNsdError("NSD ID {} error: {}".
294 self.log.debug("NSD to instantiate: {}".format(nsd))
296 # Generate a UUID for NS
297 ns_id = str(uuid.uuid4())
298 self.log.debug("NS instance uuid: {}".format(ns_id))
300 # Build the nsr post data
302 'name': self._service_name,
305 nsr['om-datacenter'] = self._dc
307 nsr['cloud-account'] = self._account
309 data = {'nsr': [nsr]}
311 data_str = json.dumps(data)
312 self.log.debug("NSR post data: {}".format(data_str))
314 inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
315 format(url=self._conf_url, data=data_str)
316 output = self._exec_cmd(inst_url)
317 self.log.debug("Instantiate output: {}".format(output))
319 js = json.loads(output)
321 if "last-error" in js:
322 msg = "Error instantiating NS as {} with NSD {}: ". \
323 format(self._service_name, self._nsd_id,
326 raise OnboardPkgInstError(msg)
328 elif "rpc-reply" in js:
329 reply = js["rpc-reply"]
330 if "rpc-error" in reply:
331 msg = "Error instantiating NS as {} with NSD {}: ". \
332 format(self._service_name, self._nsd_id,
335 raise OnboardPkgInstError(msg)
337 self.log.info("Successfully initiated instantiation of NS as {} ({})".
338 format(self._service_name, ns_id))
342 self.validate_connectivity()
343 self.upload_packages()
347 if __name__ == "__main__":
348 parser = argparse.ArgumentParser(description='Upload and instantiate NS')
349 parser.add_argument("-s", "--so-ip", default='localhost',
350 help="SO Launchpad IP")
352 parser.add_argument("-u", "--upload-pkg", action='append',
353 help="Descriptor packages to upload. " + \
354 "If multiple descriptors are provided, they are uploaded in the same sequence.")
356 parser.add_argument("-i", "--instantiate",
357 help="Instantiate a network service with the name")
358 parser.add_argument("-d", "--nsd-id",
359 help="Network descriptor ID to instantiate")
360 parser.add_argument("-D", "--datacenter",
361 help="OpenMano datacenter to instantiate on")
362 parser.add_argument("-c", "--vim-account",
363 help="Cloud/VIM account to instantiate on")
365 parser.add_argument("-p", "--upload-port", default=4567, type=int,
366 help="Upload port number, default 4567")
367 parser.add_argument("-P", "--restconf-port", default=8888, type=int,
368 help="RESTconf port number, default 8888")
369 parser.add_argument("--restconf-user", default='admin',
370 help="RESTconf user name, default admin")
371 parser.add_argument("--restconf-password", default='admin',
372 help="RESTconf password, default admin")
374 parser.add_argument("-v", "--verbose", action='store_true',
375 help="Show more logs")
377 args = parser.parse_args()
379 fmt = logging.Formatter(
380 '%(asctime)-23s %(levelname)-5s (%(name)s@%(process)d:' \
381 '%(filename)s:%(lineno)d) - %(message)s')
382 stderr_handler = logging.StreamHandler(stream=sys.stderr)
383 stderr_handler.setFormatter(fmt)
384 logging.basicConfig(level=logging.INFO)
385 log = logging.getLogger('onboard-pkg')
386 log.addHandler(stderr_handler)
388 log.setLevel(logging.DEBUG)
390 log.debug("Input arguments: {}".format(args))
392 ob = OnboardPackage(log, args)