RAPI: Implement OS parameters for instance reinstallation
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API version 2 baserlib.library.
23
24   PUT or POST?
25   ============
26
27   According to RFC2616 the main difference between PUT and POST is that
28   POST can create new resources but PUT can only create the resource the
29   URI was pointing to on the PUT request.
30
31   To be in context of this module for instance creation POST on
32   /2/instances is legitim while PUT would be not, due to it does create a
33   new entity and not just replace /2/instances with it.
34
35   So when adding new methods, if they are operating on the URI entity itself,
36   PUT should be prefered over POST.
37
38 """
39
40 # pylint: disable-msg=C0103
41
42 # C0103: Invalid name, since the R_* names are not conforming
43
44 from ganeti import opcodes
45 from ganeti import http
46 from ganeti import constants
47 from ganeti import cli
48 from ganeti import utils
49 from ganeti import rapi
50 from ganeti.rapi import baserlib
51
52
53 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
54 I_FIELDS = ["name", "admin_state", "os",
55             "pnode", "snodes",
56             "disk_template",
57             "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
58             "network_port",
59             "disk.sizes", "disk_usage",
60             "beparams", "hvparams",
61             "oper_state", "oper_ram", "oper_vcpus", "status",
62             "custom_hvparams", "custom_beparams", "custom_nicparams",
63             ] + _COMMON_FIELDS
64
65 N_FIELDS = ["name", "offline", "master_candidate", "drained",
66             "dtotal", "dfree",
67             "mtotal", "mnode", "mfree",
68             "pinst_cnt", "sinst_cnt",
69             "ctotal", "cnodes", "csockets",
70             "pip", "sip", "role",
71             "pinst_list", "sinst_list",
72             "master_capable", "vm_capable",
73             "group.uuid",
74             ] + _COMMON_FIELDS
75
76 _NR_DRAINED = "drained"
77 _NR_MASTER_CANDIATE = "master-candidate"
78 _NR_MASTER = "master"
79 _NR_OFFLINE = "offline"
80 _NR_REGULAR = "regular"
81
82 _NR_MAP = {
83   "M": _NR_MASTER,
84   "C": _NR_MASTER_CANDIATE,
85   "D": _NR_DRAINED,
86   "O": _NR_OFFLINE,
87   "R": _NR_REGULAR,
88   }
89
90 # Request data version field
91 _REQ_DATA_VERSION = "__version__"
92
93 # Feature string for instance creation request data version 1
94 _INST_CREATE_REQV1 = "instance-create-reqv1"
95
96 # Feature string for instance reinstall request version 1
97 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
98
99 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
100 _WFJC_TIMEOUT = 10
101
102
103 class R_version(baserlib.R_Generic):
104   """/version resource.
105
106   This resource should be used to determine the remote API version and
107   to adapt clients accordingly.
108
109   """
110   @staticmethod
111   def GET():
112     """Returns the remote API version.
113
114     """
115     return constants.RAPI_VERSION
116
117
118 class R_2_info(baserlib.R_Generic):
119   """Cluster info.
120
121   """
122   @staticmethod
123   def GET():
124     """Returns cluster information.
125
126     """
127     client = baserlib.GetClient()
128     return client.QueryClusterInfo()
129
130
131 class R_2_features(baserlib.R_Generic):
132   """/2/features resource.
133
134   """
135   @staticmethod
136   def GET():
137     """Returns list of optional RAPI features implemented.
138
139     """
140     return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1]
141
142
143 class R_2_os(baserlib.R_Generic):
144   """/2/os resource.
145
146   """
147   @staticmethod
148   def GET():
149     """Return a list of all OSes.
150
151     Can return error 500 in case of a problem.
152
153     Example: ["debian-etch"]
154
155     """
156     cl = baserlib.GetClient()
157     op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
158     job_id = baserlib.SubmitJob([op], cl)
159     # we use custom feedback function, instead of print we log the status
160     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
161     diagnose_data = result[0]
162
163     if not isinstance(diagnose_data, list):
164       raise http.HttpBadGateway(message="Can't get OS list")
165
166     os_names = []
167     for (name, variants) in diagnose_data:
168       os_names.extend(cli.CalculateOSNames(name, variants))
169
170     return os_names
171
172
173 class R_2_redist_config(baserlib.R_Generic):
174   """/2/redistribute-config resource.
175
176   """
177   @staticmethod
178   def PUT():
179     """Redistribute configuration to all nodes.
180
181     """
182     return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
183
184
185 class R_2_jobs(baserlib.R_Generic):
186   """/2/jobs resource.
187
188   """
189   @staticmethod
190   def GET():
191     """Returns a dictionary of jobs.
192
193     @return: a dictionary with jobs id and uri.
194
195     """
196     fields = ["id"]
197     cl = baserlib.GetClient()
198     # Convert the list of lists to the list of ids
199     result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
200     return baserlib.BuildUriList(result, "/2/jobs/%s",
201                                  uri_fields=("id", "uri"))
202
203
204 class R_2_jobs_id(baserlib.R_Generic):
205   """/2/jobs/[job_id] resource.
206
207   """
208   def GET(self):
209     """Returns a job status.
210
211     @return: a dictionary with job parameters.
212         The result includes:
213             - id: job ID as a number
214             - status: current job status as a string
215             - ops: involved OpCodes as a list of dictionaries for each
216               opcodes in the job
217             - opstatus: OpCodes status as a list
218             - opresult: OpCodes results as a list of lists
219
220     """
221     fields = ["id", "ops", "status", "summary",
222               "opstatus", "opresult", "oplog",
223               "received_ts", "start_ts", "end_ts",
224               ]
225     job_id = self.items[0]
226     result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
227     if result is None:
228       raise http.HttpNotFound()
229     return baserlib.MapFields(fields, result)
230
231   def DELETE(self):
232     """Cancel not-yet-started job.
233
234     """
235     job_id = self.items[0]
236     result = baserlib.GetClient().CancelJob(job_id)
237     return result
238
239
240 class R_2_jobs_id_wait(baserlib.R_Generic):
241   """/2/jobs/[job_id]/wait resource.
242
243   """
244   # WaitForJobChange provides access to sensitive information and blocks
245   # machine resources (it's a blocking RAPI call), hence restricting access.
246   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
247
248   def GET(self):
249     """Waits for job changes.
250
251     """
252     job_id = self.items[0]
253
254     fields = self.getBodyParameter("fields")
255     prev_job_info = self.getBodyParameter("previous_job_info", None)
256     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
257
258     if not isinstance(fields, list):
259       raise http.HttpBadRequest("The 'fields' parameter should be a list")
260
261     if not (prev_job_info is None or isinstance(prev_job_info, list)):
262       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
263                                 " be a list")
264
265     if not (prev_log_serial is None or
266             isinstance(prev_log_serial, (int, long))):
267       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
268                                 " be a number")
269
270     client = baserlib.GetClient()
271     result = client.WaitForJobChangeOnce(job_id, fields,
272                                          prev_job_info, prev_log_serial,
273                                          timeout=_WFJC_TIMEOUT)
274     if not result:
275       raise http.HttpNotFound()
276
277     if result == constants.JOB_NOTCHANGED:
278       # No changes
279       return None
280
281     (job_info, log_entries) = result
282
283     return {
284       "job_info": job_info,
285       "log_entries": log_entries,
286       }
287
288
289 class R_2_nodes(baserlib.R_Generic):
290   """/2/nodes resource.
291
292   """
293   def GET(self):
294     """Returns a list of all nodes.
295
296     """
297     client = baserlib.GetClient()
298
299     if self.useBulk():
300       bulkdata = client.QueryNodes([], N_FIELDS, False)
301       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
302     else:
303       nodesdata = client.QueryNodes([], ["name"], False)
304       nodeslist = [row[0] for row in nodesdata]
305       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
306                                    uri_fields=("id", "uri"))
307
308
309 class R_2_nodes_name(baserlib.R_Generic):
310   """/2/nodes/[node_name] resources.
311
312   """
313   def GET(self):
314     """Send information about a node.
315
316     """
317     node_name = self.items[0]
318     client = baserlib.GetClient()
319
320     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
321                                             names=[node_name], fields=N_FIELDS,
322                                             use_locking=self.useLocking())
323
324     return baserlib.MapFields(N_FIELDS, result[0])
325
326
327 class R_2_nodes_name_role(baserlib.R_Generic):
328   """ /2/nodes/[node_name]/role resource.
329
330   """
331   def GET(self):
332     """Returns the current node role.
333
334     @return: Node role
335
336     """
337     node_name = self.items[0]
338     client = baserlib.GetClient()
339     result = client.QueryNodes(names=[node_name], fields=["role"],
340                                use_locking=self.useLocking())
341
342     return _NR_MAP[result[0][0]]
343
344   def PUT(self):
345     """Sets the node role.
346
347     @return: a job id
348
349     """
350     if not isinstance(self.request_body, basestring):
351       raise http.HttpBadRequest("Invalid body contents, not a string")
352
353     node_name = self.items[0]
354     role = self.request_body
355
356     if role == _NR_REGULAR:
357       candidate = False
358       offline = False
359       drained = False
360
361     elif role == _NR_MASTER_CANDIATE:
362       candidate = True
363       offline = drained = None
364
365     elif role == _NR_DRAINED:
366       drained = True
367       candidate = offline = None
368
369     elif role == _NR_OFFLINE:
370       offline = True
371       candidate = drained = None
372
373     else:
374       raise http.HttpBadRequest("Can't set '%s' role" % role)
375
376     op = opcodes.OpSetNodeParams(node_name=node_name,
377                                  master_candidate=candidate,
378                                  offline=offline,
379                                  drained=drained,
380                                  force=bool(self.useForce()))
381
382     return baserlib.SubmitJob([op])
383
384
385 class R_2_nodes_name_evacuate(baserlib.R_Generic):
386   """/2/nodes/[node_name]/evacuate resource.
387
388   """
389   def POST(self):
390     """Evacuate all secondary instances off a node.
391
392     """
393     node_name = self.items[0]
394     remote_node = self._checkStringVariable("remote_node", default=None)
395     iallocator = self._checkStringVariable("iallocator", default=None)
396     early_r = bool(self._checkIntVariable("early_release", default=0))
397     dry_run = bool(self.dryRun())
398
399     cl = baserlib.GetClient()
400
401     op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
402                                           iallocator=iallocator,
403                                           remote_node=remote_node)
404
405     job_id = baserlib.SubmitJob([op], cl)
406     # we use custom feedback function, instead of print we log the status
407     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
408
409     jobs = []
410     for iname, node in result:
411       if dry_run:
412         jid = None
413       else:
414         op = opcodes.OpReplaceDisks(instance_name=iname,
415                                     remote_node=node, disks=[],
416                                     mode=constants.REPLACE_DISK_CHG,
417                                     early_release=early_r)
418         jid = baserlib.SubmitJob([op])
419       jobs.append((jid, iname, node))
420
421     return jobs
422
423
424 class R_2_nodes_name_migrate(baserlib.R_Generic):
425   """/2/nodes/[node_name]/migrate resource.
426
427   """
428   def POST(self):
429     """Migrate all primary instances from a node.
430
431     """
432     node_name = self.items[0]
433
434     if "live" in self.queryargs and "mode" in self.queryargs:
435       raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
436                                 " be passed")
437     elif "live" in self.queryargs:
438       if self._checkIntVariable("live", default=1):
439         mode = constants.HT_MIGRATION_LIVE
440       else:
441         mode = constants.HT_MIGRATION_NONLIVE
442     else:
443       mode = self._checkStringVariable("mode", default=None)
444
445     op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
446
447     return baserlib.SubmitJob([op])
448
449
450 class R_2_nodes_name_storage(baserlib.R_Generic):
451   """/2/nodes/[node_name]/storage ressource.
452
453   """
454   # LUQueryNodeStorage acquires locks, hence restricting access to GET
455   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
456
457   def GET(self):
458     node_name = self.items[0]
459
460     storage_type = self._checkStringVariable("storage_type", None)
461     if not storage_type:
462       raise http.HttpBadRequest("Missing the required 'storage_type'"
463                                 " parameter")
464
465     output_fields = self._checkStringVariable("output_fields", None)
466     if not output_fields:
467       raise http.HttpBadRequest("Missing the required 'output_fields'"
468                                 " parameter")
469
470     op = opcodes.OpQueryNodeStorage(nodes=[node_name],
471                                     storage_type=storage_type,
472                                     output_fields=output_fields.split(","))
473     return baserlib.SubmitJob([op])
474
475
476 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
477   """/2/nodes/[node_name]/storage/modify ressource.
478
479   """
480   def PUT(self):
481     node_name = self.items[0]
482
483     storage_type = self._checkStringVariable("storage_type", None)
484     if not storage_type:
485       raise http.HttpBadRequest("Missing the required 'storage_type'"
486                                 " parameter")
487
488     name = self._checkStringVariable("name", None)
489     if not name:
490       raise http.HttpBadRequest("Missing the required 'name'"
491                                 " parameter")
492
493     changes = {}
494
495     if "allocatable" in self.queryargs:
496       changes[constants.SF_ALLOCATABLE] = \
497         bool(self._checkIntVariable("allocatable", default=1))
498
499     op = opcodes.OpModifyNodeStorage(node_name=node_name,
500                                      storage_type=storage_type,
501                                      name=name,
502                                      changes=changes)
503     return baserlib.SubmitJob([op])
504
505
506 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
507   """/2/nodes/[node_name]/storage/repair ressource.
508
509   """
510   def PUT(self):
511     node_name = self.items[0]
512
513     storage_type = self._checkStringVariable("storage_type", None)
514     if not storage_type:
515       raise http.HttpBadRequest("Missing the required 'storage_type'"
516                                 " parameter")
517
518     name = self._checkStringVariable("name", None)
519     if not name:
520       raise http.HttpBadRequest("Missing the required 'name'"
521                                 " parameter")
522
523     op = opcodes.OpRepairNodeStorage(node_name=node_name,
524                                      storage_type=storage_type,
525                                      name=name)
526     return baserlib.SubmitJob([op])
527
528
529 def _ParseInstanceCreateRequestVersion1(data, dry_run):
530   """Parses an instance creation request version 1.
531
532   @rtype: L{opcodes.OpCreateInstance}
533   @return: Instance creation opcode
534
535   """
536   # Disks
537   disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
538
539   disks = []
540   for idx, i in enumerate(disks_input):
541     baserlib.CheckType(i, dict, "Disk %d specification" % idx)
542
543     # Size is mandatory
544     try:
545       size = i[constants.IDISK_SIZE]
546     except KeyError:
547       raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
548                                 " size" % idx)
549
550     disk = {
551       constants.IDISK_SIZE: size,
552       }
553
554     # Optional disk access mode
555     try:
556       disk_access = i[constants.IDISK_MODE]
557     except KeyError:
558       pass
559     else:
560       disk[constants.IDISK_MODE] = disk_access
561
562     disks.append(disk)
563
564   assert len(disks_input) == len(disks)
565
566   # Network interfaces
567   nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
568
569   nics = []
570   for idx, i in enumerate(nics_input):
571     baserlib.CheckType(i, dict, "NIC %d specification" % idx)
572
573     nic = {}
574
575     for field in constants.INIC_PARAMS:
576       try:
577         value = i[field]
578       except KeyError:
579         continue
580
581       nic[field] = value
582
583     nics.append(nic)
584
585   assert len(nics_input) == len(nics)
586
587   # HV/BE parameters
588   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
589   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
590
591   beparams = baserlib.CheckParameter(data, "beparams", default={})
592   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
593
594   return opcodes.OpCreateInstance(
595     mode=baserlib.CheckParameter(data, "mode"),
596     instance_name=baserlib.CheckParameter(data, "name"),
597     os_type=baserlib.CheckParameter(data, "os"),
598     osparams=baserlib.CheckParameter(data, "osparams", default={}),
599     force_variant=baserlib.CheckParameter(data, "force_variant",
600                                           default=False),
601     no_install=baserlib.CheckParameter(data, "no_install", default=False),
602     pnode=baserlib.CheckParameter(data, "pnode", default=None),
603     snode=baserlib.CheckParameter(data, "snode", default=None),
604     disk_template=baserlib.CheckParameter(data, "disk_template"),
605     disks=disks,
606     nics=nics,
607     src_node=baserlib.CheckParameter(data, "src_node", default=None),
608     src_path=baserlib.CheckParameter(data, "src_path", default=None),
609     start=baserlib.CheckParameter(data, "start", default=True),
610     wait_for_sync=True,
611     ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
612     name_check=baserlib.CheckParameter(data, "name_check", default=True),
613     file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
614                                              default=None),
615     file_driver=baserlib.CheckParameter(data, "file_driver",
616                                         default=constants.FD_LOOP),
617     source_handshake=baserlib.CheckParameter(data, "source_handshake",
618                                              default=None),
619     source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
620                                            default=None),
621     source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
622                                                  default=None),
623     iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
624     hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
625     hvparams=hvparams,
626     beparams=beparams,
627     dry_run=dry_run,
628     )
629
630
631 class R_2_instances(baserlib.R_Generic):
632   """/2/instances resource.
633
634   """
635   def GET(self):
636     """Returns a list of all available instances.
637
638     """
639     client = baserlib.GetClient()
640
641     use_locking = self.useLocking()
642     if self.useBulk():
643       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
644       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
645     else:
646       instancesdata = client.QueryInstances([], ["name"], use_locking)
647       instanceslist = [row[0] for row in instancesdata]
648       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
649                                    uri_fields=("id", "uri"))
650
651   def _ParseVersion0CreateRequest(self):
652     """Parses an instance creation request version 0.
653
654     Request data version 0 is deprecated and should not be used anymore.
655
656     @rtype: L{opcodes.OpCreateInstance}
657     @return: Instance creation opcode
658
659     """
660     # Do not modify anymore, request data version 0 is deprecated
661     beparams = baserlib.MakeParamsDict(self.request_body,
662                                        constants.BES_PARAMETERS)
663     hvparams = baserlib.MakeParamsDict(self.request_body,
664                                        constants.HVS_PARAMETERS)
665     fn = self.getBodyParameter
666
667     # disk processing
668     disk_data = fn('disks')
669     if not isinstance(disk_data, list):
670       raise http.HttpBadRequest("The 'disks' parameter should be a list")
671     disks = []
672     for idx, d in enumerate(disk_data):
673       if not isinstance(d, int):
674         raise http.HttpBadRequest("Disk %d specification wrong: should"
675                                   " be an integer" % idx)
676       disks.append({"size": d})
677
678     # nic processing (one nic only)
679     nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
680     if fn("ip", None) is not None:
681       nics[0]["ip"] = fn("ip")
682     if fn("mode", None) is not None:
683       nics[0]["mode"] = fn("mode")
684     if fn("link", None) is not None:
685       nics[0]["link"] = fn("link")
686     if fn("bridge", None) is not None:
687       nics[0]["bridge"] = fn("bridge")
688
689     # Do not modify anymore, request data version 0 is deprecated
690     return opcodes.OpCreateInstance(
691       mode=constants.INSTANCE_CREATE,
692       instance_name=fn('name'),
693       disks=disks,
694       disk_template=fn('disk_template'),
695       os_type=fn('os'),
696       pnode=fn('pnode', None),
697       snode=fn('snode', None),
698       iallocator=fn('iallocator', None),
699       nics=nics,
700       start=fn('start', True),
701       ip_check=fn('ip_check', True),
702       name_check=fn('name_check', True),
703       wait_for_sync=True,
704       hypervisor=fn('hypervisor', None),
705       hvparams=hvparams,
706       beparams=beparams,
707       file_storage_dir=fn('file_storage_dir', None),
708       file_driver=fn('file_driver', constants.FD_LOOP),
709       dry_run=bool(self.dryRun()),
710       )
711
712   def POST(self):
713     """Create an instance.
714
715     @return: a job id
716
717     """
718     if not isinstance(self.request_body, dict):
719       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
720
721     # Default to request data version 0
722     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
723
724     if data_version == 0:
725       op = self._ParseVersion0CreateRequest()
726     elif data_version == 1:
727       op = _ParseInstanceCreateRequestVersion1(self.request_body,
728                                                self.dryRun())
729     else:
730       raise http.HttpBadRequest("Unsupported request data version %s" %
731                                 data_version)
732
733     return baserlib.SubmitJob([op])
734
735
736 class R_2_instances_name(baserlib.R_Generic):
737   """/2/instances/[instance_name] resources.
738
739   """
740   def GET(self):
741     """Send information about an instance.
742
743     """
744     client = baserlib.GetClient()
745     instance_name = self.items[0]
746
747     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
748                                             names=[instance_name],
749                                             fields=I_FIELDS,
750                                             use_locking=self.useLocking())
751
752     return baserlib.MapFields(I_FIELDS, result[0])
753
754   def DELETE(self):
755     """Delete an instance.
756
757     """
758     op = opcodes.OpRemoveInstance(instance_name=self.items[0],
759                                   ignore_failures=False,
760                                   dry_run=bool(self.dryRun()))
761     return baserlib.SubmitJob([op])
762
763
764 class R_2_instances_name_info(baserlib.R_Generic):
765   """/2/instances/[instance_name]/info resource.
766
767   """
768   def GET(self):
769     """Request detailed instance information.
770
771     """
772     instance_name = self.items[0]
773     static = bool(self._checkIntVariable("static", default=0))
774
775     op = opcodes.OpQueryInstanceData(instances=[instance_name],
776                                      static=static)
777     return baserlib.SubmitJob([op])
778
779
780 class R_2_instances_name_reboot(baserlib.R_Generic):
781   """/2/instances/[instance_name]/reboot resource.
782
783   Implements an instance reboot.
784
785   """
786   def POST(self):
787     """Reboot an instance.
788
789     The URI takes type=[hard|soft|full] and
790     ignore_secondaries=[False|True] parameters.
791
792     """
793     instance_name = self.items[0]
794     reboot_type = self.queryargs.get('type',
795                                      [constants.INSTANCE_REBOOT_HARD])[0]
796     ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
797     op = opcodes.OpRebootInstance(instance_name=instance_name,
798                                   reboot_type=reboot_type,
799                                   ignore_secondaries=ignore_secondaries,
800                                   dry_run=bool(self.dryRun()))
801
802     return baserlib.SubmitJob([op])
803
804
805 class R_2_instances_name_startup(baserlib.R_Generic):
806   """/2/instances/[instance_name]/startup resource.
807
808   Implements an instance startup.
809
810   """
811   def PUT(self):
812     """Startup an instance.
813
814     The URI takes force=[False|True] parameter to start the instance
815     if even if secondary disks are failing.
816
817     """
818     instance_name = self.items[0]
819     force_startup = bool(self._checkIntVariable('force'))
820     op = opcodes.OpStartupInstance(instance_name=instance_name,
821                                    force=force_startup,
822                                    dry_run=bool(self.dryRun()))
823
824     return baserlib.SubmitJob([op])
825
826
827 class R_2_instances_name_shutdown(baserlib.R_Generic):
828   """/2/instances/[instance_name]/shutdown resource.
829
830   Implements an instance shutdown.
831
832   """
833   def PUT(self):
834     """Shutdown an instance.
835
836     """
837     instance_name = self.items[0]
838     op = opcodes.OpShutdownInstance(instance_name=instance_name,
839                                     dry_run=bool(self.dryRun()))
840
841     return baserlib.SubmitJob([op])
842
843
844 def _ParseInstanceReinstallRequest(name, data):
845   """Parses a request for reinstalling an instance.
846
847   """
848   if not isinstance(data, dict):
849     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
850
851   ostype = baserlib.CheckParameter(data, "os")
852   start = baserlib.CheckParameter(data, "start", exptype=bool,
853                                   default=True)
854   osparams = baserlib.CheckParameter(data, "osparams", default=None)
855
856   ops = [
857     opcodes.OpShutdownInstance(instance_name=name),
858     opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
859                                 osparams=osparams),
860     ]
861
862   if start:
863     ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
864
865   return ops
866
867
868 class R_2_instances_name_reinstall(baserlib.R_Generic):
869   """/2/instances/[instance_name]/reinstall resource.
870
871   Implements an instance reinstall.
872
873   """
874   def POST(self):
875     """Reinstall an instance.
876
877     The URI takes os=name and nostartup=[0|1] optional
878     parameters. By default, the instance will be started
879     automatically.
880
881     """
882     if self.request_body:
883       if self.queryargs:
884         raise http.HttpBadRequest("Can't combine query and body parameters")
885
886       body = self.request_body
887     else:
888       if not self.queryargs:
889         raise http.HttpBadRequest("Missing query parameters")
890       # Legacy interface, do not modify/extend
891       body = {
892         "os": self._checkStringVariable("os"),
893         "start": not self._checkIntVariable("nostartup"),
894         }
895
896     ops = _ParseInstanceReinstallRequest(self.items[0], body)
897
898     return baserlib.SubmitJob(ops)
899
900
901 class R_2_instances_name_replace_disks(baserlib.R_Generic):
902   """/2/instances/[instance_name]/replace-disks resource.
903
904   """
905   def POST(self):
906     """Replaces disks on an instance.
907
908     """
909     instance_name = self.items[0]
910     remote_node = self._checkStringVariable("remote_node", default=None)
911     mode = self._checkStringVariable("mode", default=None)
912     raw_disks = self._checkStringVariable("disks", default=None)
913     iallocator = self._checkStringVariable("iallocator", default=None)
914
915     if raw_disks:
916       try:
917         disks = [int(part) for part in raw_disks.split(",")]
918       except ValueError, err:
919         raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
920     else:
921       disks = []
922
923     op = opcodes.OpReplaceDisks(instance_name=instance_name,
924                                 remote_node=remote_node,
925                                 mode=mode,
926                                 disks=disks,
927                                 iallocator=iallocator)
928
929     return baserlib.SubmitJob([op])
930
931
932 class R_2_instances_name_activate_disks(baserlib.R_Generic):
933   """/2/instances/[instance_name]/activate-disks resource.
934
935   """
936   def PUT(self):
937     """Activate disks for an instance.
938
939     The URI might contain ignore_size to ignore current recorded size.
940
941     """
942     instance_name = self.items[0]
943     ignore_size = bool(self._checkIntVariable('ignore_size'))
944
945     op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
946                                          ignore_size=ignore_size)
947
948     return baserlib.SubmitJob([op])
949
950
951 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
952   """/2/instances/[instance_name]/deactivate-disks resource.
953
954   """
955   def PUT(self):
956     """Deactivate disks for an instance.
957
958     """
959     instance_name = self.items[0]
960
961     op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
962
963     return baserlib.SubmitJob([op])
964
965
966 class R_2_instances_name_prepare_export(baserlib.R_Generic):
967   """/2/instances/[instance_name]/prepare-export resource.
968
969   """
970   def PUT(self):
971     """Prepares an export for an instance.
972
973     @return: a job id
974
975     """
976     instance_name = self.items[0]
977     mode = self._checkStringVariable("mode")
978
979     op = opcodes.OpPrepareExport(instance_name=instance_name,
980                                  mode=mode)
981
982     return baserlib.SubmitJob([op])
983
984
985 def _ParseExportInstanceRequest(name, data):
986   """Parses a request for an instance export.
987
988   @rtype: L{opcodes.OpExportInstance}
989   @return: Instance export opcode
990
991   """
992   mode = baserlib.CheckParameter(data, "mode",
993                                  default=constants.EXPORT_MODE_LOCAL)
994   target_node = baserlib.CheckParameter(data, "destination")
995   shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
996   remove_instance = baserlib.CheckParameter(data, "remove_instance",
997                                             exptype=bool, default=False)
998   x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
999   destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1000                                                 default=None)
1001
1002   return opcodes.OpExportInstance(instance_name=name,
1003                                   mode=mode,
1004                                   target_node=target_node,
1005                                   shutdown=shutdown,
1006                                   remove_instance=remove_instance,
1007                                   x509_key_name=x509_key_name,
1008                                   destination_x509_ca=destination_x509_ca)
1009
1010
1011 class R_2_instances_name_export(baserlib.R_Generic):
1012   """/2/instances/[instance_name]/export resource.
1013
1014   """
1015   def PUT(self):
1016     """Exports an instance.
1017
1018     @return: a job id
1019
1020     """
1021     if not isinstance(self.request_body, dict):
1022       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1023
1024     op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1025
1026     return baserlib.SubmitJob([op])
1027
1028
1029 def _ParseMigrateInstanceRequest(name, data):
1030   """Parses a request for an instance migration.
1031
1032   @rtype: L{opcodes.OpMigrateInstance}
1033   @return: Instance migration opcode
1034
1035   """
1036   mode = baserlib.CheckParameter(data, "mode", default=None)
1037   cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1038                                     default=False)
1039
1040   return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1041                                    cleanup=cleanup)
1042
1043
1044 class R_2_instances_name_migrate(baserlib.R_Generic):
1045   """/2/instances/[instance_name]/migrate resource.
1046
1047   """
1048   def PUT(self):
1049     """Migrates an instance.
1050
1051     @return: a job id
1052
1053     """
1054     baserlib.CheckType(self.request_body, dict, "Body contents")
1055
1056     op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1057
1058     return baserlib.SubmitJob([op])
1059
1060
1061 def _ParseRenameInstanceRequest(name, data):
1062   """Parses a request for renaming an instance.
1063
1064   @rtype: L{opcodes.OpRenameInstance}
1065   @return: Instance rename opcode
1066
1067   """
1068   new_name = baserlib.CheckParameter(data, "new_name")
1069   ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1070   name_check = baserlib.CheckParameter(data, "name_check", default=True)
1071
1072   return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1073                                   name_check=name_check, ip_check=ip_check)
1074
1075
1076 class R_2_instances_name_rename(baserlib.R_Generic):
1077   """/2/instances/[instance_name]/rename resource.
1078
1079   """
1080   def PUT(self):
1081     """Changes the name of an instance.
1082
1083     @return: a job id
1084
1085     """
1086     baserlib.CheckType(self.request_body, dict, "Body contents")
1087
1088     op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1089
1090     return baserlib.SubmitJob([op])
1091
1092
1093 def _ParseModifyInstanceRequest(name, data):
1094   """Parses a request for modifying an instance.
1095
1096   @rtype: L{opcodes.OpSetInstanceParams}
1097   @return: Instance modify opcode
1098
1099   """
1100   osparams = baserlib.CheckParameter(data, "osparams", default={})
1101   force = baserlib.CheckParameter(data, "force", default=False)
1102   nics = baserlib.CheckParameter(data, "nics", default=[])
1103   disks = baserlib.CheckParameter(data, "disks", default=[])
1104   disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1105   remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1106   os_name = baserlib.CheckParameter(data, "os_name", default=None)
1107   force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1108
1109   # HV/BE parameters
1110   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1111   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1112                       allowed_values=[constants.VALUE_DEFAULT])
1113
1114   beparams = baserlib.CheckParameter(data, "beparams", default={})
1115   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1116                       allowed_values=[constants.VALUE_DEFAULT])
1117
1118   return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1119                                      beparams=beparams, osparams=osparams,
1120                                      force=force, nics=nics, disks=disks,
1121                                      disk_template=disk_template,
1122                                      remote_node=remote_node, os_name=os_name,
1123                                      force_variant=force_variant)
1124
1125
1126 class R_2_instances_name_modify(baserlib.R_Generic):
1127   """/2/instances/[instance_name]/modify resource.
1128
1129   """
1130   def PUT(self):
1131     """Changes some parameters of an instance.
1132
1133     @return: a job id
1134
1135     """
1136     baserlib.CheckType(self.request_body, dict, "Body contents")
1137
1138     op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1139
1140     return baserlib.SubmitJob([op])
1141
1142
1143 class _R_Tags(baserlib.R_Generic):
1144   """ Quasiclass for tagging resources
1145
1146   Manages tags. When inheriting this class you must define the
1147   TAG_LEVEL for it.
1148
1149   """
1150   TAG_LEVEL = None
1151
1152   def __init__(self, items, queryargs, req):
1153     """A tag resource constructor.
1154
1155     We have to override the default to sort out cluster naming case.
1156
1157     """
1158     baserlib.R_Generic.__init__(self, items, queryargs, req)
1159
1160     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1161       self.name = None
1162     else:
1163       self.name = items[0]
1164
1165   def GET(self):
1166     """Returns a list of tags.
1167
1168     Example: ["tag1", "tag2", "tag3"]
1169
1170     """
1171     # pylint: disable-msg=W0212
1172     return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1173
1174   def PUT(self):
1175     """Add a set of tags.
1176
1177     The request as a list of strings should be PUT to this URI. And
1178     you'll have back a job id.
1179
1180     """
1181     # pylint: disable-msg=W0212
1182     if 'tag' not in self.queryargs:
1183       raise http.HttpBadRequest("Please specify tag(s) to add using the"
1184                                 " the 'tag' parameter")
1185     return baserlib._Tags_PUT(self.TAG_LEVEL,
1186                               self.queryargs['tag'], name=self.name,
1187                               dry_run=bool(self.dryRun()))
1188
1189   def DELETE(self):
1190     """Delete a tag.
1191
1192     In order to delete a set of tags, the DELETE
1193     request should be addressed to URI like:
1194     /tags?tag=[tag]&tag=[tag]
1195
1196     """
1197     # pylint: disable-msg=W0212
1198     if 'tag' not in self.queryargs:
1199       # no we not gonna delete all tags
1200       raise http.HttpBadRequest("Cannot delete all tags - please specify"
1201                                 " tag(s) using the 'tag' parameter")
1202     return baserlib._Tags_DELETE(self.TAG_LEVEL,
1203                                  self.queryargs['tag'],
1204                                  name=self.name,
1205                                  dry_run=bool(self.dryRun()))
1206
1207
1208 class R_2_instances_name_tags(_R_Tags):
1209   """ /2/instances/[instance_name]/tags resource.
1210
1211   Manages per-instance tags.
1212
1213   """
1214   TAG_LEVEL = constants.TAG_INSTANCE
1215
1216
1217 class R_2_nodes_name_tags(_R_Tags):
1218   """ /2/nodes/[node_name]/tags resource.
1219
1220   Manages per-node tags.
1221
1222   """
1223   TAG_LEVEL = constants.TAG_NODE
1224
1225
1226 class R_2_tags(_R_Tags):
1227   """ /2/instances/tags resource.
1228
1229   Manages cluster tags.
1230
1231   """
1232   TAG_LEVEL = constants.TAG_CLUSTER