blob: 849f9e4ac26cbe3f326996419b399d5987eb8005 [file] [log] [blame]
Philip Joseph54931522016-09-14 05:36:15 +00001#!/usr/bin/env python3
2
3############################################################################
4# Copyright 2016 RIFT.io Inc #
5# #
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 #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
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############################################################################
18
19import argparse
20from contextlib import closing
21import logging
22import os.path
23import socket
24import subprocess
25import sys
26import uuid
27
28import json
29
30
31class OnboardPkgError(Exception):
32 pass
33
34
35class OnboardPkgInputError(OnboardPkgError):
36 pass
37
38
39class OnboardPkgMissingPkg(OnboardPkgError):
40 pass
41
42
43class OnboardPkgFileError(OnboardPkgError):
44 pass
45
46
47class OnboardPkgMissingDescId(OnboardPkgError):
48 pass
49
50
51class OnboardPkgInvalidDescId(OnboardPkgError):
52 pass
53
54
55class OnboardPkgMissingAcct(OnboardPkgError):
56 pass
57
58
59class OnboardPkgSoConnError(OnboardPkgError):
60 pass
61
62
63class OnboardPkgCmdError(OnboardPkgError):
64 pass
65
66
67class OnboardPkgUploadError(OnboardPkgError):
68 pass
69
70
71class OnboardPkgRcConnError(OnboardPkgError):
72 pass
73
74
Philip Joseph18465ac2016-09-14 18:10:39 +000075class OnboardPkgDcError(OnboardPkgError):
76 pass
77
78
Philip Joseph54931522016-09-14 05:36:15 +000079class OnboardPkgAcctError(OnboardPkgError):
80 pass
81
82
83class OnboardPkgNsdError(OnboardPkgError):
84 pass
85
86
87class OnboardPkgInstError(OnboardPkgError):
88 pass
89
90
91class OnboardPkgInvalidPort(OnboardPkgError):
92 pass
93
94
95class OnboardPackage:
96
97 def __init__(self,
98 log,
99 args):
100 self._log = log
101 self._args = args
102
Philip Josephf4937572017-03-03 01:55:37 +0530103 self._project = args.project
104
Philip Joseph54931522016-09-14 05:36:15 +0000105 self._pkgs = None
106
107 self._service_name = None
108 self._nsd_id = None
109 self._dc = None
110 self._account = None
111
112 self._ip = args.so_ip
Kiran Kashalkaraf759322017-02-01 03:02:48 +0000113 self._api_server_ip = "localhost"
Philip Joseph54931522016-09-14 05:36:15 +0000114
115 self._uport = args.upload_port
Kiran Kashalkaraf759322017-02-01 03:02:48 +0000116 self._onboard_port = args.onboard_port
117 self._rport = args.restconf_port
118 self._user = args.restconf_user
119 self._password = args.restconf_password
120 self._onboard_url = "curl -k --user \"{user}:{passwd}\" \"https://{ip}:{port}/composer/upload?api_server=https://{api_server_ip}&upload_server=https://{ip}\"". \
121 format(ip=self._ip,
122 port=self._onboard_port,
123 user=self._user,
124 passwd=self._password,
125 api_server_ip=self._api_server_ip)
Philip Josephf4937572017-03-03 01:55:37 +0530126
Philip Joseph54931522016-09-14 05:36:15 +0000127 self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
128 format(ip=self._ip,
129 port=self._uport)
130
Philip Joseph54931522016-09-14 05:36:15 +0000131 self._headers = '-H "accept: application/json"' + \
132 ' -H "content-type: application/json"'
Philip Josephf4937572017-03-03 01:55:37 +0530133
134 self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config/project/{project}". \
Philip Joseph54931522016-09-14 05:36:15 +0000135 format(header=self._headers,
136 user=self._user,
137 passwd=self._password,
138 ip=self._ip,
Philip Josephf4937572017-03-03 01:55:37 +0530139 port=self._rport,
140 project=self._project)
141
142 self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational/project/{project}". \
Philip Joseph18465ac2016-09-14 18:10:39 +0000143 format(header=self._headers,
144 user=self._user,
145 passwd=self._password,
146 ip=self._ip,
Philip Josephf4937572017-03-03 01:55:37 +0530147 port=self._rport,
148 project=self._project)
Philip Joseph54931522016-09-14 05:36:15 +0000149
150 @property
151 def log(self):
152 return self._log
153
154 def validate_args(self):
Philip Josephf4937572017-03-03 01:55:37 +0530155 args = self._args
Philip Joseph54931522016-09-14 05:36:15 +0000156 if args.upload_pkg is not None:
157 self._pkgs = args.upload_pkg
158 self.log.debug("Packages to upload: {}".format(self._pkgs))
159 if len(self._pkgs) == 0:
160 raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')
161
162 for pkg in self._pkgs:
163 self.log.debug("Check pkg: {}".format(pkg))
164 if os.path.isfile(pkg) is False:
165 raise OnboardPkgFileError("Unable to access file: {}".format(pkg))
166
167 if args.instantiate:
168 if args.nsd_id is None:
169 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
170
171 if args.datacenter:
172 try:
173 uuid.UUID(args.datacenter)
174 self._dc = args.datacenter
175 except ValueError as e:
Philip Josephf4937572017-03-03 01:55:37 +0530176 raise OnboardPkgInvalidDescId("Invalid UUID for datacenter {}: {}".
177 format(args.datacenter, e))
Philip Joseph54931522016-09-14 05:36:15 +0000178
179 elif args.vim_account:
180 self._account = args.vim_account
181
182 else:
183 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
184
185 self._service_name = args.instantiate
186 self._nsd_id = args.nsd_id
187
188 self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
189 self._service_name,
190 self._account))
191
Philip Josephf4937572017-03-03 01:55:37 +0530192 if (self._pkgs is None) and (self._nsd_id is None) and (not args.list_nsds):
193 raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate or list options")
Philip Joseph54931522016-09-14 05:36:15 +0000194
195 # Validate the port numbers are correct
196 def valid_port(port):
197 if 1 <= port <= 65535:
198 return True
199 return False
200
201 if not valid_port(self._uport):
202 raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
203
204 if not valid_port(self._rport):
205 raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
206
207 def _exec_cmd(self, cmd):
208 self.log.debug("Execute command: {}".format(cmd))
Philip Joseph18465ac2016-09-14 18:10:39 +0000209 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
210 stderr=subprocess.PIPE, shell=True)
Philip Joseph54931522016-09-14 05:36:15 +0000211 (output, err) = proc.communicate()
212 rc = proc.returncode
Philip Joseph18465ac2016-09-14 18:10:39 +0000213 self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
214 format(rc, output, err))
Philip Joseph54931522016-09-14 05:36:15 +0000215 if rc != 0:
216 raise OnboardPkgCmdError("Command {} failed ({}): {}".
217 format(cmd, rc, err))
218 return output.decode("utf-8")
219
220 def validate_connectivity(self):
221 if self._pkgs:
222 self.log.debug("Check connectivity to SO at {}:{}".
223 format(self._ip, self._uport))
224
225 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
226 if sock.connect_ex((self._ip, self._uport)) != 0:
227 raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
228 format(self._ip, self._uport))
229 self.log.debug("Connection to SO upload port succeeded")
230
231 if self._nsd_id:
232 self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
233 format(self._ip, self._rport, self._user, self._password))
234
235 rest_url = self._conf_url+"/resource-orchestrator"
236 try:
237 output = self._exec_cmd(rest_url)
238 self.log.debug("Output of restconf validation: {}".
239 format(output))
240 if len(output) != 0:
241 js = json.loads(output)
242 if "error" in js:
243 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
244 format(js["error"]))
245
246 self.log.debug("Connection to SO restconf port succeeded")
247
248 except OnboardPkgCmdError as e:
249 self.log.error("SO restconf connect failed: {}".format(e))
250 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
251 format(e))
252
253
254 def _upload_package(self, pkg):
Kiran Kashalkaraf759322017-02-01 03:02:48 +0000255 upload_cmd = "{url} -F \"package=@{pkg}\" ". \
256 format(url=self._onboard_url,
Philip Joseph54931522016-09-14 05:36:15 +0000257 pkg=pkg)
258 self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
259
260 output = self._exec_cmd(upload_cmd)
261
262 # Get the transaction id and wait for upload to complete
263 tx_id = json.loads(output)['transaction_id']
264
265 upload_status_url = "{url}/{id}/state". \
266 format(url=self._upload_url,
267 id=tx_id)
268 status = ""
269 while status not in ['success', 'failure']:
270 output = self._exec_cmd(upload_status_url)
271 js = json.loads(output)
272 self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
273 status = js['status']
274
275 if status != 'success':
276 raise OnboardPkgUploadError("Package {} upload failed: {}".
277 format(pkg, js['errors']))
278
279 self.log.info("Upload of package {} succeeded".format(pkg))
280
281 def upload_packages(self):
282 if self._pkgs is None:
283 self.log.debug("Upload packages not provided")
284 return
285
286 for pkg in self._pkgs:
287 self._upload_package(pkg)
288
289 def instantiate(self):
290 if self._nsd_id is None:
291 self.log.debug("No NSD ID provided for instantiation")
292 return
293
Philip Joseph18465ac2016-09-14 18:10:39 +0000294 # Check to see if datacenter is valid
295 if self._dc:
296 dc_url = "{url}/datacenters". format(url=self._oper_url)
297 output = self._exec_cmd(dc_url)
298 if (output is None) or (len(output) == 0):
299 # Account not found
300 raise OnboardPkgDcError("Datacenter {} provided is not valid".
301 format(self._dc))
302 found = False
303 js = json.loads(output)
304 if "ro-accounts" in js["rw-launchpad:datacenters"]:
305 for ro in js["rw-launchpad:datacenters"]["ro-accounts"]:
306 if "datacenters" in ro:
307 for dc in ro["datacenters"]:
308 if dc["uuid"] == self._dc:
309 self.log.debug("Found datacenter {}".format(dc))
310 found = True
311 break
312 if found:
313 break
314
315 if found is False:
316 raise OnboardPkgDcError("Datacenter {} provided is not valid".
317 format(self._dc))
318
Philip Joseph54931522016-09-14 05:36:15 +0000319
320 # Check cloud account is valid, if provided
321 if self._account:
322 acct_url = "{url}/cloud/account/{acct}". \
323 format(url=self._conf_url, acct=self._account)
324 output = self._exec_cmd(acct_url)
325 if (output is None) or (len(output) == 0):
326 # Account not found
327 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
328 format(self._account))
329
330 # Check id NSD ID is valid
331 nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
332 format(url=self._conf_url, nsd_id=self._nsd_id)
333 output = self._exec_cmd(nsd_url)
334 if (output is None) or (len(output) == 0):
335 # NSD not found
336 raise OnboardPkgNsdError("NSD ID {} provided is not valid".
337 format(self._nsd_id))
338
339 js = json.loads(output)
340 if "error" in js:
341 raise OnboardPkgNsdError("NSD ID {} error: {}".
342 format(self._nsd_id,
343 js['error']))
344
Philip Josephf4937572017-03-03 01:55:37 +0530345 try:
Philip Joseph4f810f22017-03-07 23:09:10 +0530346 nsd = js['project-nsd:nsd']
Philip Josephf4937572017-03-03 01:55:37 +0530347 except KeyError as e:
348 raise OnboardPkgNsdError("NSD ID {} provided is not valid".
349 format(self._nsd_id))
350
Philip Joseph54931522016-09-14 05:36:15 +0000351 self.log.debug("NSD to instantiate: {}".format(nsd))
352
353 # Generate a UUID for NS
354 ns_id = str(uuid.uuid4())
355 self.log.debug("NS instance uuid: {}".format(ns_id))
356
357 # Build the nsr post data
358 nsr = {"id": ns_id,
359 'name': self._service_name,
360 "nsd": nsd,}
361 if self._dc:
362 nsr['om-datacenter'] = self._dc
363 else:
364 nsr['cloud-account'] = self._account
365
366 data = {'nsr': [nsr]}
367
368 data_str = json.dumps(data)
369 self.log.debug("NSR post data: {}".format(data_str))
370
371 inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
372 format(url=self._conf_url, data=data_str)
373 output = self._exec_cmd(inst_url)
374 self.log.debug("Instantiate output: {}".format(output))
375
376 js = json.loads(output)
377
378 if "last-error" in js:
379 msg = "Error instantiating NS as {} with NSD {}: ". \
380 format(self._service_name, self._nsd_id,
381 js["last-error"])
382 self.log.error(msg)
383 raise OnboardPkgInstError(msg)
384
385 elif "rpc-reply" in js:
386 reply = js["rpc-reply"]
387 if "rpc-error" in reply:
388 msg = "Error instantiating NS as {} with NSD {}: ". \
389 format(self._service_name, self._nsd_id,
390 reply["rpc-error"])
Philip Joseph70d92ee2017-03-28 23:52:19 +0530391 # self.log.error(msg)
Philip Joseph54931522016-09-14 05:36:15 +0000392 raise OnboardPkgInstError(msg)
393
394 self.log.info("Successfully initiated instantiation of NS as {} ({})".
395 format(self._service_name, ns_id))
396
Philip Josephf4937572017-03-03 01:55:37 +0530397 def list_nsds(self):
398 if self._args.list_nsds:
399 self.log.debug("Check NSDS at {}:{}, with credentials {}:{}".
400 format(self._ip, self._rport, self._user, self._password))
401
402 rest_url = self._conf_url+"/nsd-catalog/nsd"
403 try:
404 output = self._exec_cmd(rest_url)
405 self.log.debug("Output of NSD list: {}".
406 format(output))
407 if output:
408 js = json.loads(output)
409 if "error" in js:
410 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
411 format(js["error"]))
412 else:
413 print("No NSDs found on SO")
414 return
415
416 self.log.debug("NSD list: {}".format(js))
417 print('List of NSDs on SO:\nName\tID')
Philip Joseph4f810f22017-03-07 23:09:10 +0530418 for nsd in js['project-nsd:nsd']:
Philip Josephf4937572017-03-03 01:55:37 +0530419 print('{}\t{}'.format(nsd['name'], nsd['id']))
420
421 except OnboardPkgCmdError as e:
422 self.log.error("SO restconf connect failed: {}".format(e))
423 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
424 format(e))
425
Philip Joseph54931522016-09-14 05:36:15 +0000426 def process(self):
Philip Joseph70d92ee2017-03-28 23:52:19 +0530427 try:
428 self.validate_args()
429 except Exception as e:
430 if args.verbose:
431 log.exception(e)
432
433 print("\nERROR:", e)
434 print("\n")
435 parser.print_help()
436 sys.exit(2)
437
Philip Joseph54931522016-09-14 05:36:15 +0000438 self.validate_connectivity()
439 self.upload_packages()
440 self.instantiate()
Philip Josephf4937572017-03-03 01:55:37 +0530441 self.list_nsds()
Philip Joseph54931522016-09-14 05:36:15 +0000442
443
444if __name__ == "__main__":
445 parser = argparse.ArgumentParser(description='Upload and instantiate NS')
446 parser.add_argument("-s", "--so-ip", default='localhost',
447 help="SO Launchpad IP")
448
449 parser.add_argument("-u", "--upload-pkg", action='append',
450 help="Descriptor packages to upload. " + \
451 "If multiple descriptors are provided, they are uploaded in the same sequence.")
452
Philip Josephf4937572017-03-03 01:55:37 +0530453 parser.add_argument("-l", "--list-nsds", action='store_true',
454 help="List available network service descriptors")
455
Philip Joseph54931522016-09-14 05:36:15 +0000456 parser.add_argument("-i", "--instantiate",
457 help="Instantiate a network service with the name")
458 parser.add_argument("-d", "--nsd-id",
459 help="Network descriptor ID to instantiate")
460 parser.add_argument("-D", "--datacenter",
461 help="OpenMano datacenter to instantiate on")
462 parser.add_argument("-c", "--vim-account",
463 help="Cloud/VIM account to instantiate on")
464
Philip Josephf4937572017-03-03 01:55:37 +0530465 parser.add_argument("--project", default='default',
466 help="Project to use, default 'default'")
Kiran Kashalkaraf759322017-02-01 03:02:48 +0000467 parser.add_argument("-o", "--onboard-port", default=8443, type=int,
468 help="Onboarding port number - node port number, default 8443")
Philip Joseph54931522016-09-14 05:36:15 +0000469 parser.add_argument("-p", "--upload-port", default=4567, type=int,
470 help="Upload port number, default 4567")
Rajesh Velandy7908c6b2016-10-25 21:20:48 +0000471 parser.add_argument("-P", "--restconf-port", default=8008, type=int,
472 help="RESTconf port number, default 8008")
Philip Joseph54931522016-09-14 05:36:15 +0000473 parser.add_argument("--restconf-user", default='admin',
474 help="RESTconf user name, default admin")
475 parser.add_argument("--restconf-password", default='admin',
476 help="RESTconf password, default admin")
477
478 parser.add_argument("-v", "--verbose", action='store_true',
479 help="Show more logs")
480
481 args = parser.parse_args()
482
483 fmt = logging.Formatter(
484 '%(asctime)-23s %(levelname)-5s (%(name)s@%(process)d:' \
485 '%(filename)s:%(lineno)d) - %(message)s')
Philip Joseph54931522016-09-14 05:36:15 +0000486 log = logging.getLogger('onboard-pkg')
Philip Joseph70d92ee2017-03-28 23:52:19 +0530487 log.setLevel(logging.INFO)
Philip Joseph54931522016-09-14 05:36:15 +0000488 if args.verbose:
489 log.setLevel(logging.DEBUG)
Philip Joseph70d92ee2017-03-28 23:52:19 +0530490 ch = logging.StreamHandler()
491 ch.setLevel(logging.DEBUG)
492 ch.setFormatter(fmt)
493 log.addHandler(ch)
Philip Joseph54931522016-09-14 05:36:15 +0000494
495 log.debug("Input arguments: {}".format(args))
496
Philip Joseph70d92ee2017-03-28 23:52:19 +0530497 try:
498 ob = OnboardPackage(log, args)
499 ob.process()
500 except Exception as e:
501 if args.verbose:
502 log.exception(e)
503
504 print("\nERROR:", e)
505 sys.exit(1)
506