Merge "Bug 38 - SO:Add support for E1000 virtual interfaces"
[osm/SO.git] / rwlaunchpad / plugins / rwlaunchpadtasklet / scripts / onboard_pkg
1 #!/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
19 import argparse
20 from contextlib import closing
21 import logging
22 import os.path
23 import socket
24 import subprocess
25 import sys
26 import uuid
27
28 import json
29
30
31 class OnboardPkgError(Exception):
32     pass
33
34
35 class OnboardPkgInputError(OnboardPkgError):
36     pass
37
38
39 class OnboardPkgMissingPkg(OnboardPkgError):
40     pass
41
42
43 class OnboardPkgFileError(OnboardPkgError):
44     pass
45
46
47 class OnboardPkgMissingDescId(OnboardPkgError):
48     pass
49
50
51 class OnboardPkgInvalidDescId(OnboardPkgError):
52     pass
53
54
55 class OnboardPkgMissingAcct(OnboardPkgError):
56     pass
57
58
59 class OnboardPkgSoConnError(OnboardPkgError):
60     pass
61
62
63 class OnboardPkgCmdError(OnboardPkgError):
64     pass
65
66
67 class OnboardPkgUploadError(OnboardPkgError):
68     pass
69
70
71 class OnboardPkgRcConnError(OnboardPkgError):
72     pass
73
74
75 class OnboardPkgAcctError(OnboardPkgError):
76     pass
77
78
79 class OnboardPkgNsdError(OnboardPkgError):
80     pass
81
82
83 class OnboardPkgInstError(OnboardPkgError):
84     pass
85
86
87 class OnboardPkgInvalidPort(OnboardPkgError):
88     pass
89
90
91 class OnboardPackage:
92
93     def __init__(self,
94                  log,
95                  args):
96         self._log = log
97         self._args = args
98
99         self._pkgs = None
100
101         self._service_name = None
102         self._nsd_id = None
103         self._dc = None
104         self._account = None
105
106         self._ip = args.so_ip
107
108         self._uport = args.upload_port
109         self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
110                             format(ip=self._ip,
111                                    port=self._uport)
112
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,
120                               user=self._user,
121                               passwd=self._password,
122                               ip=self._ip,
123                               port=self._rport)
124
125     @property
126     def log(self):
127         return self._log
128
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')
135
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))
140
141         if args.instantiate:
142             if args.nsd_id is None:
143                 raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")
144
145             if args.datacenter:
146                 try:
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))
152
153             elif args.vim_account:
154                 self._account = args.vim_account
155
156             else:
157                 raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation")
158
159             self._service_name = args.instantiate
160             self._nsd_id = args.nsd_id
161
162             self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
163                                                                    self._service_name,
164                                                                    self._account))
165
166         if (self._pkgs is None) and (self._nsd_id is None):
167             raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate options")
168
169         # Validate the port numbers are correct
170         def valid_port(port):
171             if 1 <= port <= 65535:
172                 return True
173             return False
174
175         if not valid_port(self._uport):
176             raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))
177
178         if not valid_port(self._rport):
179             raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))
180
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()
185         rc = proc.returncode
186         self.log.debug("Command exec status: {}, {}, {}".format(rc, output, err))
187         if rc != 0:
188             raise OnboardPkgCmdError("Command {} failed ({}): {}".
189                                             format(cmd, rc, err))
190         return output.decode("utf-8")
191
192     def validate_connectivity(self):
193         if self._pkgs:
194             self.log.debug("Check connectivity to SO at {}:{}".
195                            format(self._ip, self._uport))
196
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")
202
203         if self._nsd_id:
204             self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
205                            format(self._ip, self._rport, self._user, self._password))
206
207             rest_url = self._conf_url+"/resource-orchestrator"
208             try:
209                 output = self._exec_cmd(rest_url)
210                 self.log.debug("Output of restconf validation: {}".
211                                format(output))
212                 if len(output) != 0:
213                     js = json.loads(output)
214                     if "error" in js:
215                         raise OnboardPkgRcConnError("SO Restconf connect error: {}".
216                                                     format(js["error"]))
217
218                 self.log.debug("Connection to SO restconf port succeeded")
219
220             except OnboardPkgCmdError as e:
221                 self.log.error("SO restconf connect failed: {}".format(e))
222                 raise OnboardPkgRcConnError("SO Restconf connect error: {}".
223                                             format(e))
224
225
226     def _upload_package(self, pkg):
227         upload_cmd = "{url} -F \"descriptor=@{pkg}\" ". \
228                                           format(url=self._upload_url,
229                                                  pkg=pkg)
230         self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))
231
232         output = self._exec_cmd(upload_cmd)
233
234         # Get the transaction id and wait for upload to complete
235         tx_id = json.loads(output)['transaction_id']
236
237         upload_status_url = "{url}/{id}/state". \
238                             format(url=self._upload_url,
239                                    id=tx_id)
240         status = ""
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']
246
247         if status != 'success':
248             raise OnboardPkgUploadError("Package {} upload failed: {}".
249                                         format(pkg, js['errors']))
250
251         self.log.info("Upload of package {} succeeded".format(pkg))
252
253     def upload_packages(self):
254         if self._pkgs is None:
255             self.log.debug("Upload packages not provided")
256             return
257
258         for pkg in self._pkgs:
259             self._upload_package(pkg)
260
261     def instantiate(self):
262         if self._nsd_id is None:
263             self.log.debug("No NSD ID provided for instantiation")
264             return
265
266         # TODO: Add check to see if datacenter is valid
267
268         # Check cloud account is valid, if provided
269         if self._account:
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):
274                 # Account not found
275                 raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid".
276                                           format(self._account))
277
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):
283             # NSD not found
284             raise OnboardPkgNsdError("NSD ID {} provided is not valid".
285                                      format(self._nsd_id))
286
287         js = json.loads(output)
288         if "error" in js:
289             raise OnboardPkgNsdError("NSD ID {} error: {}".
290                                      format(self._nsd_id,
291                                             js['error']))
292
293         nsd = js['nsd:nsd']
294         self.log.debug("NSD to instantiate: {}".format(nsd))
295
296         # Generate a UUID for NS
297         ns_id = str(uuid.uuid4())
298         self.log.debug("NS instance uuid: {}".format(ns_id))
299
300         # Build the nsr post data
301         nsr = {"id": ns_id,
302                'name': self._service_name,
303                "nsd": nsd,}
304         if self._dc:
305             nsr['om-datacenter'] = self._dc
306         else:
307             nsr['cloud-account'] = self._account
308
309         data = {'nsr': [nsr]}
310
311         data_str = json.dumps(data)
312         self.log.debug("NSR post data: {}".format(data_str))
313
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))
318
319         js = json.loads(output)
320
321         if "last-error" in js:
322             msg = "Error instantiating NS as {} with NSD {}: ". \
323                   format(self._service_name, self._nsd_id,
324                          js["last-error"])
325             self.log.error(msg)
326             raise OnboardPkgInstError(msg)
327
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,
333                          reply["rpc-error"])
334                 self.log.error(msg)
335                 raise OnboardPkgInstError(msg)
336
337         self.log.info("Successfully initiated instantiation of NS as {} ({})".
338                       format(self._service_name, ns_id))
339
340     def process(self):
341         self.validate_args()
342         self.validate_connectivity()
343         self.upload_packages()
344         self.instantiate()
345
346
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")
351
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.")
355
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")
364
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")
373
374     parser.add_argument("-v", "--verbose", action='store_true',
375                         help="Show more logs")
376
377     args = parser.parse_args()
378
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)
387     if args.verbose:
388         log.setLevel(logging.DEBUG)
389
390     log.debug("Input arguments: {}".format(args))
391
392     ob = OnboardPackage(log, args)
393     ob.process()