diff --git a/computes/views.py b/computes/views.py index bb9c55e..7bc7700 100644 --- a/computes/views.py +++ b/computes/views.py @@ -177,7 +177,7 @@ def compute_graph(request, compute_id): datasets = {} cookies = {} compute = get_object_or_404(Compute, pk=compute_id) - curent_time = time.strftime("%H:%M:%S") + current_time = time.strftime("%H:%M:%S") try: conn = wvmHostDetails(compute.hostname, @@ -208,7 +208,7 @@ def compute_graph(request, compute_id): datasets['mem'] = eval(cookies['mem']) datasets['timer'] = eval(cookies['timer']) - datasets['timer'].append(curent_time) + datasets['timer'].append(current_time) datasets['cpu'].append(int(cpu_usage['usage'])) datasets['mem'].append(int(mem_usage['usage']) / 1048576) diff --git a/conf/requirements.txt b/conf/requirements.txt index 49903c0..d8c5a5f 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,7 +1,7 @@ -Django==1.11.14 +Django==1.11.17 websockify==0.8.0 gunicorn==19.9.0 -lxml==4.2.3 -libvirt-python==4.4.0 +lxml==4.2.5 +libvirt-python==4.10.0 pytz diff --git a/create/templates/create_instance.html b/create/templates/create_instance.html index 34ee73c..bb3b0a2 100644 --- a/create/templates/create_instance.html +++ b/create/templates/create_instance.html @@ -441,7 +441,7 @@ <label class="col-sm-3 control-label">{% trans "HDD" %}</label> <input id="images" name="images" type="hidden" value=""/> <div class="col-sm-3"> - <select id="storage" name="storage" class="form-control" onchange="get_template_vols({{ compute_id }}, value);"> + <select class="form-control" onchange="get_template_vols({{ compute_id }}, value);"> {% if storages %} <option value disabled selected>{% trans "Select pool" %}...</option> {% for storage in storages %} @@ -458,6 +458,20 @@ </select> </div> </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Storage" %}</label> + <div class="col-sm-7"> + <select id="storage" name="storage" class="form-control" disabled> + {% if storages %} + {% for storage in storages %} + <option value="{{ storage }}" >{{ storage }}</option> + {% endfor %} + {% else %} + <option value="">{% trans "None" %}</option> + {% endif %} + </select> + </div> + </div> <div class="form-group meta-prealloc"> <label class="col-sm-3 control-label">{% trans "Metadata" %}</label> <div class="col-sm-7"> @@ -697,6 +711,9 @@ }); }); $("#template").removeAttr("disabled"); + $("#storage").val(pool).change(); + $("#storage").removeAttr("disabled"); + } function get_disk_bus_choices(compute_id, dev_idx, disk_type){ diff --git a/create/views.py b/create/views.py index 0bb2133..9bd881a 100644 --- a/create/views.py +++ b/create/views.py @@ -134,7 +134,7 @@ def create_instance(request, compute_id): error_msg = _("Image has already exist. Please check volumes or change instance name") error_messages.append(error_msg) else: - clone_path = conn.clone_from_template(data['name'], templ_path, metadata=meta_prealloc) + clone_path = conn.clone_from_template(data['name'], templ_path, data['storage'], metadata=meta_prealloc) volume = dict() volume['path'] = clone_path volume['type'] = conn.get_volume_type(clone_path) diff --git a/instances/templates/add_instance_volume.html b/instances/templates/add_instance_volume.html index 9cea2fc..6b0fed3 100644 --- a/instances/templates/add_instance_volume.html +++ b/instances/templates/add_instance_volume.html @@ -1,6 +1,6 @@ {% load i18n %} {% if request.user.is_superuser %} -<a href="#addvol" type="button" class="btn btn-success pull-right" data-toggle="modal"> +<a href="#addvol" type="button" class="btn btn-success pull-right" data-toggle="modal" title="Add Volume"> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> </a> diff --git a/instances/templates/instance.html b/instances/templates/instance.html index f0a6388..bd6bb78 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -40,7 +40,7 @@ </div> </div> {% if user_quota_msg %} - <span class="label label-warning">{{ user_quota_msg|capfirst }} quota reached.</span> + <span class="label label-warning">{{ user_quota_msg|capfirst }} {% trans "quota reached" %}.</span> {% endif %} <hr> </div> @@ -187,23 +187,18 @@ {% endif %} {% endifequal %} {% ifequal status 3 %} - {% if request.user.is_superuser %} - <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resume"> + <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resume"> + <form action="" method="post" role="form">{% csrf_token %} + {% if request.user.is_superuser %} <p>{% trans "This action restore the instance after suspend." %}</p> - <form action="" method="post" role="form">{% csrf_token %} - <input type="submit" name="resume" class="btn btn-lg btn-success pull-right" value="{% trans "Resume" %}"> - <div class="clearfix"></div> - </form> - </div> - {% else %} - <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resume"> + <input type="submit" name="resume" class="btn btn-lg btn-success pull-right" value="{% trans "Resume" %}"> + {% else %} <p>{% trans "Administrator blocked your instance." %}</p> - <form action="" method="post" role="form">{% csrf_token %} - <button class="btn btn-lg btn-success disabled pull-right">{% trans "Resume" %}</button> - <div class="clearfix"></div> - </form> - </div> - {% endif %} + <button class="btn btn-lg btn-success disabled pull-right">{% trans "Resume" %}</button> + {% endif %} + <div class="clearfix"></div> + </form> + </div> {% endifequal %} {% ifequal status 5 %} <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot"> @@ -270,7 +265,6 @@ <li><a href="#" title="Console port: {{ console_port }}" onclick="open_console('full')">{% trans "Console - Full" %}</a></li> </ul> </div> - {% else %} <button class="btn btn-lg btn-success pull-right disabled">{% trans "Console" %}</button> {% endifequal %} @@ -350,7 +344,7 @@ <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resizevm"> {% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %} <form class="form-horizontal" method="post" role="form">{% csrf_token %} - <p style="font-weight:bold;">{% trans "Logical host CPUs:" %} {{ vcpu_host }}</p> + <p style="font-weight:bold;">{% trans "Logical host CPUs" %} : {{ vcpu_host }}</p> <div class="form-group"> <label class="col-sm-4 control-label" style="font-weight:normal;"> {% trans "Current allocation" %}</label> <div class="col-sm-4"> @@ -381,8 +375,7 @@ <div class="col-sm-4 js-custom__container"> <select name="cur_memory" class="form-control js-custom__toggle"> {% for mem in memory_range %} - <option value="{{ mem }}" - {% if mem == cur_memory %}selected{% endif %}>{{ mem }}</option> + <option value="{{ mem }}" {% if mem == cur_memory %}selected{% endif %}>{{ mem }}</option> {% endfor %} </select> <input type="text" name="cur_memory_custom" class="form-control js-custom__toggle" style="display: none" /> @@ -392,7 +385,6 @@ <div class="form-group"> <label class="col-sm-4 control-label" style="font-weight:normal;">{% trans "Maximum allocation" %} ({% trans "MB" %})</label> - <div class="col-sm-4 js-custom__container"> <select name="memory" class="form-control js-custom__toggle"> {% for mem in memory_range %} @@ -425,7 +417,6 @@ {% endif %} <div class="clearfix"></div> </div> - </div> </div> </div> @@ -473,11 +464,11 @@ <div class="table-responsive"> <table class="table"> <thead> - <tr> - <th>{% trans "Name" %}</th> - <th>{% trans "Date" %}</th> - <th colspan="2">{% trans "Action" %}</th> - </tr> + <tr> + <th>{% trans "Name" %}</th> + <th>{% trans "Date" %}</th> + <th colspan="2">{% trans "Action" %}</th> + </tr> </thead> <tbody> {% for snap in snapshots %} @@ -526,8 +517,8 @@ <!-- Nav tabs --> <ul class="nav nav-tabs" role="tablist"> <li role="presentation" class="active"> - <a href="#media" aria-controls="media" role="tab" data-toggle="tab"> - {% trans "Media" %} + <a href="#boot_opt" aria-controls="boot" role="tab" data-toggle="tab"> + {% trans "Boot" %} </a> </li> <li role="presentation"> @@ -535,13 +526,6 @@ {% trans "Disk" %} </a> </li> - {% if request.user.is_superuser %} - <li role="presentation"> - <a href="#autostart" aria-controls="autostart" role="tab" data-toggle="tab"> - {% trans "Autostart" %} - </a> - </li> - {% endif %} {% if request.user.is_superuser or userinstance.is_vnc %} <li role="presentation"> @@ -593,78 +577,150 @@ </ul> <!-- Tab panes --> <div class="tab-content"> - <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="media"> - {% if request.user.is_superuser and status == 5 %} - <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} - <div class="form-group"> - <div class="col-sm-12"> - <button type="submit" name="add_cdrom" type="button" class="btn btn-success pull-right"> - <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> - </button> - </div> - </div> - </form> - {% endif %} - {% for cd in media %} + {% if request.user.is_superuser %} + <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot_opt"> + <p style="font-weight:bold;">{% trans 'Autostart' %}</p> <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> - <a class="col-sm-2 control-label" - name="details" - title="{% trans "Details" %}" - tabindex="0" - data-trigger="focus" - data-toggle="popover" - data-html="true" - data-content="<strong>Bus:</strong> {{ cd.bus }} <br/> <strong>Dev:</strong> {{ cd.dev }}"> - {% trans "CDROM" %} {{ forloop.counter }} - </a> - {% if not cd.image %} - <div class="col-sm-6"> - <select name="media" class="form-control"> - {% if media_iso %} - {% for iso in media_iso %} - <option value="{{ iso }}">{{ iso }}</option> - {% endfor %} - {% else %} - <option value="none">{% trans "None" %}</option> - {% endif %} - </select> - </div> - <div class="col-sm-2"> - {% if media_iso and allow_admin_or_not_template %} - <button type="submit" class="btn btn-sm btn-success pull-left" name="mount_iso" value="{{ cd.dev }}" style="margin-top: 2px;">{% trans "Mount" %}</button> - {% if status == 5 %} - <button type="submit" class="btn btn-sm btn-danger pull-left" name="detach_cdrom" value="{{ cd.dev }}" style="margin-top: 2px;"><i class="glyphicon glyphicon-remove-circle"></i></button> - {% endif %} - {% else %} - <button class="btn btn-sm btn-success pull-left disabled" style="margin-top: 2px;">{% trans "Mount" %}</button> - {% endif %} - </div> + <div class="col-sm-12 col-sm-offset-2"> + <p>{% trans "Autostart your instance when host server is power on " %} + {% ifequal autostart 0 %} + <input type="submit" class="btn btn-success" name="set_autostart" value="{% trans "Enable" %}"> {% else %} - <div class="col-sm-6"> - <input class="form-control" value="{{ cd.image }}" disabled/> - </div> - <div class="col-sm-2"> - <input type="hidden" name="path" value="{{ cd.path }}"> - {% if allow_admin_or_not_template %} - <button type="submit" class="btn btn-sm btn-success pull-left" value="{{ cd.dev }}" name="umount_iso" style="margin-top: 2px;">{% trans "Umount" %}</button> - {% else %} - <button class="btn btn-sm btn-success pull-left disabled" value="{{ cd.dev }}" name="umount_iso" style="margin-top: 2px;">{% trans "Umount" %}</button> - {% endif %} - </div> - {% endif %} + <input type="submit" class="btn btn-danger" name="unset_autostart" value="{% trans "Disable" %}"> + {% endifequal %} + </p> + </div> </div> </form> - {% endfor %} - <div class="clearfix"></div> - </div> - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="disks"> - <p style="font-weight:bold;"> - {% trans "Instance Volumes" %} - {% include 'add_instance_volume.html' %} - </p> + <p style="font-weight:bold;">{% trans 'Boot Order' %}</p> + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <div class="form-group"> + <div class="col-sm-12 col-sm-offset-2"> + {% ifequal status 5 %} + <p>{% trans "Enable Boot Menu for your instance when it starts up " %} + {% ifequal bootmenu 0 %} + <input type="submit" class="btn btn-success" name="set_bootmenu" title="Show boot menu" value="{% trans "Enable" %}"> + {% else %} + <input type="submit" class="btn btn-danger" name="unset_bootmenu" title="Hide boot menu" value="{% trans "Disable" %}"> + {% endifequal %} + {% else %} + <p>{% trans "**** Please shutdown instance to modify boot menu ****" %} + {% endifequal %} + </p> + </div> + </div> + </form> + {% ifequal bootmenu 1 %} + <div class="col-sm-6 col-sm-offset-2"> + <div class="well"> + {% for idx, val in boot_order.items %} + <label>{{ idx|add:1 }}:{{ val.target }}, </label> + {% endfor %} + </div> + </div> + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <input id="bootorder" name="bootorder" hidden> + <div class="form-group"> + <div class="col-sm-6 col-sm-offset-2"> + <div id="b_order" class="multiselect"> + {% for disk in disks %} + <label><input type="checkbox" name="disk:{{ disk.dev }}" value="disk:{{ disk.dev }}" onclick="set_orderlist($('#bootorder'))" />{{ disk.dev }} - {{ disk.image }}</label> + {% endfor %} + {% for cd in media %} + <label><input type="checkbox" name="cdrom:{{ cd.dev }}" value="cdrom:{{ cd.dev }}" onclick="set_orderlist($('#bootorder'))"/>{{ cd.dev }} - {{ cd.image }}</label> + {% endfor %} + {% for net in networks %} + <label><input type="checkbox" name="network:{{ net.mac }}" value="network:{{ net.mac }}" onclick="set_orderlist($('#bootorder'))"/>NIC - {{ net.mac|slice:"9:" }}</label> + {% endfor %} + </div> + </div> + <div class="col-sm-3"> + <div class="row" style="margin-top: 2em;"> + <a href="#" id="boot_order_up" class="btn btn-default"><span class="glyphicon glyphicon-arrow-up" title="up: move selected devices"></span></a> + </div> + <div class="row" style="margin-top: 1em;"> + <a href="#" id="boot_order_down" class="btn btn-default"><span class="glyphicon glyphicon-arrow-down" title="down: move selected devices"></span></a> + </div> + </div> + </div> + <div class="col-sm-6 col-sm-offset-2"> + <input type="submit" class="btn btn-success btn-block" name="set_bootorder" value="{% trans "Apply" %}"> + </div> + </form> + {% endifequal %} + <div class="clearfix"></div> + </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="disks"> + {% if status == 5 %} + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <p style="font-weight:bold;"> + {% trans "Instance Media" %} + <button type="submit" name="add_cdrom" type="button" class="btn btn-success pull-right" title="Add CD-Rom"> + <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> + </button> + </p> + </form> + {% endif %} + {% for cd in media %} + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <div class="form-group"> + <a class="col-sm-3 control-label" + name="details" + title="{% trans "Details" %}" + tabindex="0" + data-trigger="focus" + data-toggle="popover" + data-html="true" + data-content="<strong>{% trans 'Bus' %}:</strong> {{ cd.bus }} <br/> + <strong>{% trans 'Dev' %}:</strong> {{ cd.dev }}"> + {% trans "CDROM" %} {{ forloop.counter }} + </a> + {% if not cd.image %} + <div class="col-sm-6"> + <select name="media" class="form-control"> + {% if media_iso %} + {% for iso in media_iso %} + <option value="{{ iso }}">{{ iso }}</option> + {% endfor %} + {% else %} + <option value="none">{% trans "None" %}</option> + {% endif %} + </select> + </div> + <div class="col-sm-2"> + {% if media_iso and allow_admin_or_not_template %} + <button type="submit" class="btn btn-sm btn-success pull-left" name="mount_iso" value="{{ cd.dev }}" style="margin-top: 2px;">{% trans "Mount" %}</button> + {% else %} + <button class="btn btn-sm btn-success pull-left disabled" style="margin-top: 2px;">{% trans "Mount" %}</button> + {% endif %} + {% if status == 5 and allow_admin_or_not_template %} + <button type="submit" class="btn btn-sm btn-danger pull-left" title="Detach CD-Rom (remove device)" name="detach_cdrom" value="{{ cd.dev }}" style="margin-top: 2px;"><i class="glyphicon glyphicon-remove-circle"></i></button> + {% endif %} + </div> + {% else %} + <div class="col-sm-6"> + <input class="form-control" value="{{ cd.image }}" disabled/> + </div> + <div class="col-sm-2"> + <input type="hidden" name="path" value="{{ cd.path }}"> + {% if allow_admin_or_not_template %} + <button type="submit" class="btn btn-sm btn-success pull-left" value="{{ cd.dev }}" name="umount_iso" style="margin-top: 2px;">{% trans "Umount" %}</button> + {% else %} + <button class="btn btn-sm btn-success pull-left disabled" value="{{ cd.dev }}" name="umount_iso" style="margin-top: 2px;">{% trans "Umount" %}</button> + {% endif %} + </div> + {% endif %} + </div> + </form> + {% endfor %} - <div class="col-xs-12 col-sm-12"> + <p style="font-weight:bold;"> + {% trans "Instance Volume" %} + {% include 'add_instance_volume.html' %} + </p> + + <div class="col-xs-12 col-sm-12"> <table class="table table-hover"> <thead> <th>{% trans "Device" %}</th> @@ -685,7 +741,9 @@ data-trigger="focus" data-toggle="popover" data-html="true" - data-content="<strong>Bus:</strong> {{ disk.bus }} <br/> <strong>Format:</strong> {{ disk.format }}"> + data-content="<strong>Bus:</strong> {{ disk.bus }} <br/> + <strong>Format:</strong> {{ disk.format }} <br/> + <strong>Cache:</strong> {{ disk.cache }}"> <i class="fa fa-info"></i> </button> {{ disk.dev }} @@ -724,21 +782,167 @@ </tbody> </table> </div> - <div class="clearfix"></div> - </div> + <div class="clearfix"></div> + </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="network"> + <p> + {% trans "Assign network device to bridge" %} + {% include 'add_instance_network_block.html' %} + </p> + <p style="font-weight:bold;">{% trans "Network devices" %}</p> + <div class="col-xs-12 col-sm-12"> + <form method="post" role="form">{% csrf_token %} + {% for network in networks %} + <div class="panel panel-default"> + <div class="panel-heading"> + <label>eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})</label> + </div> + <div class="panel-body"> + <div class="form-group form-inline"> + <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "MAC" %} </label> + <input class="form-control" type="text" value="{{ network.mac }}" readonly/> + <label class="control-label"><em>to</em></label> + <input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> + </div> + <div class="form-group form-inline"> + <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "NIC" %} </label> + <input class="form-control" type="text" value="{{ network.nic }}" readonly/> + <label class="control-label"><em>to</em></label> + <select class="form-control" name="net-source-{{ forloop.counter0 }}"> + {% for c_net in compute_networks %} + <option value="net:{{ c_net }}" {% ifequal c_net network.nic %} selected {% endifequal %}>Network {{ c_net }}</option> + {% endfor %} + {% for c_iface in compute_interfaces %} + <option value="iface:{{ c_iface }}" {% ifequal c_iface network.nic %} selected {% endifequal %}>Interface {{ c_iface }}</option> + {% endfor %} + </select> + </div> + <div class="form-group form-inline"> + <label class="col-sm-2 col-sm-offset-1">{% trans "Filter" %} </label> + <input class="form-control" type="text" value="{{ network.filterref }}" readonly/> + <label class="control-label"><em>to</em></label> + <select class="form-control" name="net-nwfilter-{{ forloop.counter0 }}"> + <option value="">{% trans "None" %}</option> + {% for c_filters in compute_nwfilters %} + <option value="{{ c_filters }}" {% ifequal c_filters network.filterref %} selected {% endifequal %}>{{ c_filters }}</option> + {% endfor %} + </select> + </div> + </div> - {% if request.user.is_superuser %} - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="autostart"> - <p>{% trans "Autostart your instance when host server is power on" %}</p> - <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} - {% ifequal autostart 0 %} - <input type="submit" class="btn btn-lg btn-success pull-right" name="set_autostart" value="{% trans "Enable" %}"> + <div class="panel-footer"> + <button class="btn btn-sm btn-primary" name="change_network" title="{% trans "Change" %}">{% trans "Change" %}</button> + <button class="btn btn-sm pull-right btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">{% trans "Delete" %}</button> + </div> + </div> + {% endfor %} + </form> + </div> + <div class="clearfix"></div> + + </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="migrate"> + <p>{% trans "For migration both host servers must have equal settings and OS type" %}</p> + <form class="form-horizontal" method="post" role="form">{% csrf_token %} + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Original host" %}</label> + <div class="col-sm-6"> + <p style="margin: 10px -10px 0 0;">{{ compute.name }}</p> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Host migration" %}</label> + <div class="col-sm-6"> + <select name="compute_id" class="form-control"> + {% if computes_count != 1 %} + {% for comp in computes %} + {% if comp.id != compute.id %} + <option value="{{ comp.id }}">{{ comp.name }}</option> + {% endif %} + {% endfor %} + {% endif %} + </select> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Live migration" %}</label> + <div class="col-sm-6"> + <input type="checkbox" name="live_migrate" value="true" id="vm_live_migrate" {% ifequal status 1 %}checked{% endifequal %}> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Unsafe migration" %}</label> + <div class="col-sm-6"> + <input type="checkbox" name="unsafe_migrate" value="true" id="vm_unsafe_migrate"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Delete original" %}</label> + <div class="col-sm-6"> + <input type="checkbox" name="xml_delete" value="true" id="xml_delete" checked> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Offline migration" %}</label> + <div class="col-sm-6"> + <input type="checkbox" name="offline_migrate" value="true" id="offline_migrate" {% ifequal status 5 %}checked{% endifequal %}> + </div> + </div> + {% if computes_count != 1 %} + <button type="submit" class="btn btn-lg btn-success pull-right" name="migrate" onclick="showPleaseWaitDialog();">{% trans "Migrate" %}</button> {% else %} - <input type="submit" class="btn btn-lg btn-success pull-right" name="unset_autostart" value="{% trans "Disable" %}"> + <button class="btn btn-lg btn-success pull-right disabled">{% trans "Migrate" %}</button> + {% endif %} + </form> + <div class="clearfix"></div></p> + </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="xmledit"> + <p>{% trans "If you need to edit xml please Power Off the instance" %}</p> + <form class="form-horizontal" method="post" role="form">{% csrf_token %} + <div class="col-sm-12" id="xmlheight"> + <textarea id="editor">{{ inst_xml }}</textarea> + </div> + {% ifequal status 5 %} + <input type="hidden" name="inst_xml"> + <button type="submit" class="btn btn-lg btn-success pull-right" name="change_xml"> + {% trans "Change" %} + </button> + {% else %} + <button class="btn btn-lg btn-success pull-right disabled"> + {% trans "Change" %} + </button> {% endifequal %} </form> <div class="clearfix"></div> </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="users"> + <div> + <p style="font-weight:bold;"> + {% trans "Instance owners" %} + {% include 'add_instance_owner_block.html' %} + </p> + </div> + <div class="table-responsive"> + <table class="table table-striped sortable-theme-bootstrap" data-sortable> + <tbody class="searchable"> + {% for userinstance in userinstances %} + <tr> + <td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td> + <td style="width:30px;"> + <form action="" method="post" style="height:10px" role="form">{% csrf_token %} + <input type="hidden" name="userinstance" value="{{ userinstance.pk }}"> + <button type="submit" class="btn btn-sm btn-default" name="del_owner" title="{% trans "Delete" %}"> + <i class="fa fa-trash"></i> + </button> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <div class="clearfix"></div> + </div> {% endif %} {% if request.user.is_superuser or userinstance.is_vnc %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="vncsettings"> @@ -855,65 +1059,6 @@ <div class="clearfix"></div> </div> {% endif %} - {% if request.user.is_superuser %} - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="network"> - <p> - {% trans "Assign network device to bridge" %} - {% include 'add_instance_network_block.html' %} - </p> - <p style="font-weight:bold;">{% trans "Network devices" %}</p> - <div class="col-xs-12 col-sm-12"> - <form method="post" role="form">{% csrf_token %} - {% for network in networks %} - <div class="panel panel-default"> - <div class="panel-heading"> - <label>eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})</label> - </div> - <div class="panel-body"> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "MAC" %} </label> - <input class="form-control" type="text" value="{{ network.mac }}" readonly/> - <label class="control-label"><em>to</em></label> - <input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> - </div> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "NIC" %} </label> - <input class="form-control" type="text" value="{{ network.nic }}" readonly/> - <label class="control-label"><em>to</em></label> - <select class="form-control" name="net-source-{{ forloop.counter0 }}"> - {% for c_net in compute_networks %} - <option value="net:{{ c_net }}" {% ifequal c_net network.nic %} selected {% endifequal %}>Network {{ c_net }}</option> - {% endfor %} - {% for c_iface in compute_interfaces %} - <option value="iface:{{ c_iface }}" {% ifequal c_iface network.nic %} selected {% endifequal %}>Interface {{ c_iface }}</option> - {% endfor %} - </select> - </div> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1">{% trans "Filter" %} </label> - <input class="form-control" type="text" value="{{ network.filterref }}" readonly/> - <label class="control-label"><em>to</em></label> - <select class="form-control" name="net-nwfilter-{{ forloop.counter0 }}"> - <option value="">{% trans "None" %}</option> - {% for c_filters in compute_nwfilters %} - <option value="{{ c_filters }}" {% ifequal c_filters network.filterref %} selected {% endifequal %}>{{ c_filters }}</option> - {% endfor %} - </select> - </div> - </div> - - <div class="panel-footer"> - <button class="btn btn-sm btn-primary" name="change_network" title="{% trans "Change" %}">{% trans "Change" %}</button> - <button class="btn btn-sm pull-right btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">{% trans "Delete" %}</button> - </div> - </div> - {% endfor %} - </form> - </div> - <div class="clearfix"></div> - - </div> - {% endif %} {% if request.user.is_superuser or request.user.userattributes.can_clone_instances %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="clone"> <p style="font-weight:bold;">{% trans "Create a clone" %}</p> @@ -921,25 +1066,25 @@ <div class="form-group"> <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Clone Name" %}</label> {% if request.user.is_superuser %} - <div class="col-sm-4"> - <input id="clone_name" type="text" class="form-control" name="name" value="{{ vname }}-clone"/> - </div> - <div class="col-sm-4"> - <button type="button" class="btn btn-sm btn-success pull-left" name="guess-clone-name" - onclick="guess_clone_name()" style="margin-top: 2px;">{% trans "Guess" %}</button> - </div> + <div class="col-sm-4"> + <input id="clone_name" type="text" class="form-control" name="name" value="{{ vname }}-clone"/> + </div> + <div class="col-sm-4"> + <button type="button" class="btn btn-sm btn-success pull-left" name="guess-clone-name" + onclick="guess_clone_name()" style="margin-top: 2px;">{% trans "Guess" %}</button> + </div> {% elif clone_instance_auto_name %} - <div class="col-sm-4"> - <input id="clone_instance_auto_name" type="text" class="form-control" name="auto_name" value="Automatic" disabled/> - </div> + <div class="col-sm-4"> + <input id="clone_instance_auto_name" type="text" class="form-control" name="auto_name" value="Automatic" disabled/> + </div> {% else %} - <div class="col-sm-4"> - <select id="select_clone_name" class="form-control" name="name" size="1"/> - {% for name in clone_free_names %} - <option value="{{ name }}">{{ name }}</option> - {% endfor %} - </select> - </div> + <div class="col-sm-4"> + <select id="select_clone_name" class="form-control" name="name" size="1"/> + {% for name in clone_free_names %} + <option value="{{ name }}">{{ name }}</option> + {% endfor %} + </select> + </div> {% endif %} </div> {% if request.user.is_superuser %} @@ -1004,84 +1149,6 @@ </form> <div class="clearfix"></div> </div> - {% endif %} - {% if request.user.is_superuser %} - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="migrate"> - <p>{% trans "For migration both host servers must have equal settings and OS type" %}</p> - <form class="form-horizontal" method="post" role="form">{% csrf_token %} - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Original host" %}</label> - <div class="col-sm-6"> - <p style="margin: 10px -10px 0 0;">{{ compute.name }}</p> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Host migration" %}</label> - <div class="col-sm-6"> - <select name="compute_id" class="form-control"> - {% if computes_count != 1 %} - {% for comp in computes %} - {% if comp.id != compute.id %} - <option value="{{ comp.id }}">{{ comp.name }}</option> - {% endif %} - {% endfor %} - {% endif %} - </select> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Live migration" %}</label> - <div class="col-sm-6"> - <input type="checkbox" name="live_migrate" value="true" id="vm_live_migrate" {% ifequal status 1 %}checked{% endifequal %}> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Unsafe migration" %}</label> - <div class="col-sm-6"> - <input type="checkbox" name="unsafe_migrate" value="true" id="vm_unsafe_migrate"> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Delete original" %}</label> - <div class="col-sm-6"> - <input type="checkbox" name="xml_delete" value="true" id="xml_delete" checked> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">{% trans "Offline migration" %}</label> - <div class="col-sm-6"> - <input type="checkbox" name="offline_migrate" value="true" id="offline_migrate" {% ifequal status 5 %}checked{% endifequal %}> - </div> - </div> - {% if computes_count != 1 %} - <button type="submit" class="btn btn-lg btn-success pull-right" name="migrate" onclick="showPleaseWaitDialog();">{% trans "Migrate" %}</button> - {% else %} - <button class="btn btn-lg btn-success pull-right disabled">{% trans "Migrate" %}</button> - {% endif %} - </form> - <div class="clearfix"></div></p> - </div> - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="xmledit"> - <p>{% trans "If you need to edit xml please Power Off the instance" %}</p> - <form class="form-horizontal" method="post" role="form">{% csrf_token %} - <div class="col-sm-12" id="xmlheight"> - <textarea id="editor">{{ inst_xml }}</textarea> - </div> - {% ifequal status 5 %} - <input type="hidden" name="inst_xml"> - <button type="submit" class="btn btn-lg btn-success pull-right" name="change_xml"> - {% trans "Change" %} - </button> - {% else %} - <button class="btn btn-lg btn-success pull-right disabled"> - {% trans "Change" %} - </button> - {% endifequal %} - </form> - <div class="clearfix"></div> - </div> - {% endif %} - {% if request.user.is_superuser or request.user.userattributes.can_clone_instances %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="options"> <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> @@ -1111,36 +1178,6 @@ <div class="clearfix"></div> </div> {% endif %} - {% if request.user.is_superuser %} - <div role="tabpanel" class="tab-pane tab-pane-bordered" id="users"> - <div> - <p style="font-weight:bold;"> - {% trans "Instance owners" %} - {% include 'add_instance_owner_block.html' %} - </p> - </div> - <div class="table-responsive"> - <table class="table table-striped sortable-theme-bootstrap" data-sortable> - <tbody class="searchable"> - {% for userinstance in userinstances %} - <tr> - <td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td> - <td style="width:30px;"> - <form action="" method="post" style="height:10px" role="form">{% csrf_token %} - <input type="hidden" name="userinstance" value="{{ userinstance.pk }}"> - <button type="submit" class="btn btn-sm btn-default" name="del_owner" title="{% trans "Delete" %}"> - <i class="fa fa-trash"></i> - </button> - </form> - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - <div class="clearfix"></div> - </div> - {% endif %} </div> </div> </div> @@ -1215,7 +1252,7 @@ </tr> </thead> <tbody class="searchable"> - <tr><td colspan="3"><i>None ...</i></td></tr> + <tr><td colspan="3"><i>{% trans 'None' %}...</i></td></tr> </tbody> </table> </div> @@ -1269,8 +1306,6 @@ <script src="{% static "js/ace.js" %}" type="text/javascript" charset="utf-8"></script> <script> function get_volumes(compute_id, pool) { - - get_vol_url = "/computes/" + compute_id + "/storage/" + pool + "/volumes"; $.getJSON(get_vol_url, function (data) { $("#vols").find('option').remove(); @@ -1279,8 +1314,7 @@ $.each(data['vols'], function(i, item) { $("#vols").append('<option value=' + item +'>' + item + '</option>'); - }); - + }) }); var sto_drop = document.getElementById('select_storage'); @@ -1290,7 +1324,6 @@ var sto_input = document.getElementById('selected_storage'); sto_input.value = pool; sto_input.innerHTML = pool; - } </script> @@ -1413,7 +1446,6 @@ }); $(document).ready(function () { // set vdi url - $.get("{% url 'vdi_url' vname %}", function(data) { $("#vdi_url_input").attr("value", data); $("#vdi_url").attr("href", data); @@ -1443,6 +1475,69 @@ $(document).ready(function(){ }); }); </script> +<script> + function set_orderlist(obj){ + var result = ''; + $('#b_order label input:checked').each(function () { + if (result != '') result += ','; + result += $(this).val(); + }); + obj.val(result); + } +$(document).ready(function () { + {# Boot Order Arragements #} + jQuery.fn.multiselect = function() { + $(this).each(function() { + var checkboxes = $(this).find("input:checkbox"); + checkboxes.each(function() { + var checkbox = $(this); + // Highlight pre-selected checkboxes + if (checkbox.prop("checked")) + checkbox.parent().addClass("multiselect-on"); + // Highlight checkboxes that the user selects + checkbox.click(function() { + if (checkbox.prop("checked")) + checkbox.parent().addClass("multiselect-on"); + else + checkbox.parent().removeClass("multiselect-on"); + }); + }); + }); + }; + $(function() { + $(".multiselect").multiselect(); + }); + + $('#boot_order_up').bind('click', function() { + $('#b_order label input:checked').each( function() { + var label = $(this).parent(); + var newPos = label.index() - 1; + if (newPos > -1) { + $('#b_order label').eq(newPos).before("<label><input type='checkbox' value='"+$(this).val()+"' name='"+$(this).val()+"' checked>"+$(this).parent().text()+"</label>"); + label.remove(); + } + $(".multiselect").multiselect(); + }); + set_orderlist($("#bootorder")); + }); + + $('#boot_order_down').bind('click', function() { + var countOptions = $('#b_order label').size(); + var countSelected = $('#b_order label input:checked').size(); + $('#b_order label input:checked').each( function() { + var label = $(this).parent(); + var newPos = label.index() + countSelected; + if (newPos < countOptions) { + $('#b_order label').eq(newPos).after("<label><input type='checkbox' value='"+$(this).val()+"' name='"+$(this).val()+"' checked>"+$(this).parent().text()+"</label>"); + label.remove(); + } + $(".multiselect").multiselect(); + }); + set_orderlist($("#bootorder")); + }); + +}); +</script> <script> $(function () { $('.js-custom__checkbox').change(function () { @@ -1617,7 +1712,7 @@ $(document).ready(function(){ } }); } - if (~$.inArray(hash, ['#media', "#disks", '#network', '#clone', '#autostart', '#xmledit', '#vncsettings', '#migrate', '#options', '#users'])) { + if (~$.inArray(hash, ['#boot_opt', "#disks", '#network', '#clone', '#xmledit', '#vncsettings', '#migrate', '#options', '#users'])) { var btnsect = $('#navbtn>li>a'); $(btnsect).each(function () { if ($(this).attr('href') === '#settings') { diff --git a/instances/views.py b/instances/views.py index 9ee9540..e3c7628 100644 --- a/instances/views.py +++ b/instances/views.py @@ -52,16 +52,16 @@ def allinstances(request): else: for comp in computes: try: - all_host_vms.update(get_host_instances(request,comp)) + all_host_vms.update(get_host_instances(request, comp)) except libvirtError as lib_err: error_messages.append(lib_err) if request.method == 'POST': try: - instances_actions(request) + return instances_actions(request) except libvirtError as lib_err: error_messages.append(lib_err) - addlogmsg(request.user.username, instance.name, lib_err.message) + addlogmsg(request.user.username, request.POST.get("name", "instance"), lib_err.message) view_style = settings.VIEW_INSTANCES_LIST_STYLE @@ -88,10 +88,10 @@ def instances(request, compute_id): if request.method == 'POST': try: - instances_actions(request) + return instances_actions(request) except libvirtError as lib_err: error_messages.append(lib_err) - addlogmsg(request.user.username, instance.name, lib_err.message) + addlogmsg(request.user.username, request.POST.get("name", "instance"), lib_err.message) return render(request, 'instances.html', locals()) @@ -255,6 +255,8 @@ def instance(request, compute_id, vname): compute_nwfilters = conn.get_nwfilters() status = conn.get_status() autostart = conn.get_autostart() + bootmenu = conn.get_bootmenu() + boot_order = conn.get_bootorder() vcpu = conn.get_vcpu() cur_vcpu = conn.get_cur_vcpu() uuid = conn.get_uuid() @@ -525,7 +527,7 @@ def instance(request, compute_id, vname): conn.attach_disk("", target, device='cdrom', cache='none', targetbus=bus) msg = _('Add CD-Rom: ' + target) addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#media') + return HttpResponseRedirect(request.get_full_path() + '#disks') if 'detach_cdrom' in request.POST and allow_admin_or_not_template: dev = request.POST.get('detach_cdrom', '') @@ -533,7 +535,7 @@ def instance(request, compute_id, vname): conn.detach_disk(dev) msg = _('Detach CD-Rom: ' + dev) addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#media') + return HttpResponseRedirect(request.get_full_path() + '#disks') if 'umount_iso' in request.POST and allow_admin_or_not_template: image = request.POST.get('path', '') @@ -541,7 +543,7 @@ def instance(request, compute_id, vname): conn.umount_iso(dev, image) msg = _("Mount media: " + dev) addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#media') + return HttpResponseRedirect(request.get_full_path() + '#disks') if 'mount_iso' in request.POST and allow_admin_or_not_template: image = request.POST.get('media', '') @@ -549,7 +551,7 @@ def instance(request, compute_id, vname): conn.mount_iso(dev, image) msg = _("Umount media: " + dev) addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#media') + return HttpResponseRedirect(request.get_full_path() + '#disks') if 'snapshot' in request.POST and allow_admin_or_not_template: name = request.POST.get('name', '') @@ -591,13 +593,37 @@ def instance(request, compute_id, vname): conn.set_autostart(1) msg = _("Set autostart") addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#autostart') + return HttpResponseRedirect(request.get_full_path() + '#boot_opt') if 'unset_autostart' in request.POST: conn.set_autostart(0) msg = _("Unset autostart") addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#autostart') + return HttpResponseRedirect(request.get_full_path() + '#boot_opt') + + if 'set_bootmenu' in request.POST: + conn.set_bootmenu(1) + msg = _("Enable boot menu") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#boot_opt') + + if 'unset_bootmenu' in request.POST: + conn.set_bootmenu(0) + msg = _("Disable boot menu") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#boot_opt') + + if 'set_bootorder' in request.POST: + bootorder = request.POST.get('bootorder', '') + if bootorder: + order_list = {} + for idx, val in enumerate(bootorder.split(',')): + type, dev = val.split(':', 1) + order_list[idx] = {"type": type, "dev": dev} + conn.set_bootorder(order_list) + msg = _("Set boot order") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#boot_opt') if 'change_xml' in request.POST: exit_xml = request.POST.get('inst_xml', '') @@ -802,7 +828,6 @@ def instance(request, compute_id, vname): msg = _("Edit options") addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(request.get_full_path() + '#options') - conn.close() except libvirtError as lib_err: @@ -837,7 +862,7 @@ def inst_status(request, compute_id, vname): return response -def get_host_instances(request,comp): +def get_host_instances(request, comp): def refresh_instance_database(comp, inst_name, info): def get_userinstances_info(instance): @@ -883,7 +908,6 @@ def get_host_instances(request,comp): status = connection_manager.host_is_up(comp.type, comp.hostname) if status: - conn = wvmHostDetails(comp, comp.login, comp.password, comp.type) comp_node_info = conn.get_node_info() comp_mem = conn.get_memory_usage() @@ -907,6 +931,7 @@ def get_host_instances(request,comp): return all_host_vms + def get_user_instances(request): all_user_vms = {} user_instances = UserInstance.objects.filter(user_id=request.user.id) @@ -975,7 +1000,6 @@ def instances_actions(request): return response if request.user.is_superuser: - if 'suspend' in request.POST: msg = _("Suspend") addlogmsg(request.user.username, instance.name, msg) @@ -1003,7 +1027,7 @@ def inst_graph(request, compute_id, vname): datasets_net = {} cookies = {} points = 5 - curent_time = time.strftime("%H:%M:%S") + current_time = time.strftime("%H:%M:%S") compute = get_object_or_404(Compute, pk=compute_id) response = HttpResponse() response['Content-Type'] = "text/javascript" @@ -1030,18 +1054,15 @@ def inst_graph(request, compute_id, vname): cookies['net'] = request.COOKIES['net'] cookies['timer'] = request.COOKIES['timer'] except KeyError: - cookies['cpu'] = None - cookies['blk'] = None - cookies['net'] = None + cookies['cpu'] = cookies['blk'] = cookies['net'] = None if not cookies['cpu']: - datasets['cpu'] = [0] * points - datasets['timer'] = [0] * points + datasets['timer'] = datasets['cpu'] = [0] * points else: datasets['cpu'] = eval(cookies['cpu']) datasets['timer'] = eval(cookies['timer']) - datasets['timer'].append(curent_time) + datasets['timer'].append(current_time) datasets['cpu'].append(int(cpu_usage['cpu'])) datasets['timer'] = check_points(datasets['timer']) @@ -1049,8 +1070,7 @@ def inst_graph(request, compute_id, vname): for blk in blk_usage: if not cookies['blk']: - datasets_wr = [0] * points - datasets_rd = [0] * points + datasets_rd = datasets_wr = [0] * points else: datasets['blk'] = eval(cookies['blk']) datasets_rd = datasets['blk'][blk['dev']][0] @@ -1067,8 +1087,7 @@ def inst_graph(request, compute_id, vname): for net in net_usage: if not cookies['net']: - datasets_rx = [0] * points - datasets_tx = [0] * points + datasets_tx = datasets_rx = [0] * points else: datasets['net'] = eval(cookies['net']) datasets_rx = datasets['net'][net['dev']][0] @@ -1133,7 +1152,7 @@ def _get_random_mac_address(): @login_required def random_mac_address(request): - data = {} + data = dict() data['mac'] = _get_random_mac_address() return HttpResponse(json.dumps(data)) @@ -1157,9 +1176,9 @@ def guess_clone_name(request): @login_required def check_instance(request, vname): - check_instance = Instance.objects.filter(name=vname) + instance = Instance.objects.filter(name=vname) data = {'vname': vname, 'exists': False} - if check_instance: + if instance: data['exists'] = True return HttpResponse(json.dumps(data)) diff --git a/networks/templates/modify_fixed_address.html b/networks/templates/modify_fixed_address.html new file mode 100644 index 0000000..63c42f4 --- /dev/null +++ b/networks/templates/modify_fixed_address.html @@ -0,0 +1,53 @@ +{% load i18n %} +{% if request.user.is_superuser %} + <a href="#AddFixedNet" type="button" class="btn btn-success pull-right" data-toggle="modal"> + <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> + </a> + + <!-- Modal pool --> + <div class="modal fade" id="AddFixedNet" tabindex="-1" role="dialog" aria-labelledby="AddFixedNetLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">{% trans "Add Fixed Address" %}</h4> + </div> + <form method="post" action="" role="form">{% csrf_token %} + <div class="modal-body"> + <div class="row"> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Subnet Pool" %}</label> + <div class="col-sm-6"> + <input type="text" readonly class="form-control" name="subnet" value="{{ ipv4_network }}" required pattern="[0-9\/\.]+"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Name" %}</label> + <div class="col-sm-6"> + <input type="text" class="form-control" name="name" required pattern="[a-zA-Z0-9_]+"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Address" %}</label> + <div class="col-sm-6"> + <input type="text" class="form-control" name="address" required pattern="[0-9\/\.]+"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "MAC" %}</label> + <div class="col-sm-6"> + <input type="text" class="form-control" name="mac" required pattern="[0-9\/\:]+"> + </div> + </div> + + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button> + <button type="submit" class="btn btn-primary" name="modify_fixed_address">{% trans "Create" %}</button> + </div> + </form> + </div> <!-- /.modal-content --> + </div> <!-- /.modal-dialog --> + </div> <!-- /.modal --> +{% endif %} \ No newline at end of file diff --git a/networks/templates/network.html b/networks/templates/network.html index 6fa0c2e..450c393 100644 --- a/networks/templates/network.html +++ b/networks/templates/network.html @@ -34,6 +34,7 @@ <!-- /.row --> {% include 'errors_block.html' %} + {% include 'messages_block.html' %} <div class="row"> <div class="col-xs-6 col-sm-4"> @@ -66,9 +67,11 @@ </p> </div> </div> - {% if state %} + {#{% if state %}#} + <div class="row"> + <h3 class="page-header">{% trans "IPv4 Configuration" %}</h3> + </div> <div class="row"> - <h3 class="page-header">{% trans "IPv4 configuration" %}</h3> <div class="col-xs-6 col-sm-4"> <p>{% trans "IPv4 Forwarding:" %}</p> <p>{% trans "Network:" %}</p> @@ -102,43 +105,81 @@ {% endif %} </p> {% if ipv4_dhcp_range_start and ipv4_dhcp_range_end %} - <p>{{ ipv4_dhcp_range_start }}</p> - <p>{{ ipv4_dhcp_range_end }}</p> + <form method="post" role="form">{% csrf_token %} + {% if state %} + <p>{{ ipv4_dhcp_range_start }}</p> + <p>{{ ipv4_dhcp_range_end }}</p> + {% else %} + <p><input name="range_start" value="{{ ipv4_dhcp_range_start }}"/></p> + <p><input name="range_end" value="{{ ipv4_dhcp_range_end }}"/></p> + <div class="col-xs-10 col-sm-8"> + <input type="submit" class="btn btn-primary btn-block" value="Apply" + name="modify_dhcp_range" + title="Edit DHCP Range" onclick="return confirm('{% trans "Are you sure?" %}')"/> + </div> + {% endif %} + </form> {% endif %} </div> </div> - {% if fixed_address %} + {% ifequal ipv4_forward.0 'nat' %} + {% if state %} + {% include 'modify_fixed_address.html' %} + {% endif %} <div class="row"> <h3 class="page-header">{% trans "Fixed Address" %}</h3> + </div> + {% endifequal %} + {% if fixed_address %} + <div class="row"> <div class="col-xs-12"> <div class="panel-group" id="accordion"> <div class="panel panel-default"> <div class="panel-heading"> <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne"> - Show + {% trans 'Show' %} </a> </div> <div id="collapseOne" class="panel-collapse collapse"> <div class="panel-body"> - <div class="input-append form-inline pull-right" style=""> + + <div class="input-append form-inline pull-right"> <div class="form-group"> <input type="text" class="form-control" id="filter_input"> </div> <input type="button" class="btn btn-default" id="filter_button" value="Filter"> - <button type="button" class="btn btn-default" id="filter_clear">Clear</button> + <button type="button" class="btn btn-default" id="filter_clear">{% trans 'Clear' %}</button> </div> <table class="table table-hover"> <thead> <tr> - <th style="text-align: center">{% trans "Address" %}</th> <th style="text-align: center">{% trans "MAC" %}</th> + <th style="text-align: center">{% trans "Address" %}</th> + <th style="text-align: center">{% trans "Name" %}</th> + <th style="text-align: center">{% trans "Action" %}</th> </tr> </thead> <tbody style="text-align: center"> {% for fix in fixed_address %} <tr> - <td>{{ fix.host }}</td> - <td>{{ fix.mac }}</td> + <form method="post" role="form">{% csrf_token %} + <td><label class="form-control" disabled="true">{{ fix.mac }}</label></td> + <td><input class="form-control" value="{{ fix.ip }}" name="address" /></td> + <td><input class="form-control" value="{{ fix.name }}" name="name" /></td> + <td> + <input hidden name="mac" value="{{ fix.mac }}"/> + <button type="submit" class="btn btn-sm btn-primary" + name="modify_fixed_address" + title="Edit entry" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-save"></i> + </button> + <button type="submit" class="btn btn-sm btn-danger" + name="delete_fixed_address" + title="Delete entry" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-trash"></i> + </button> + </td> + </form> </tr> {% endfor %} </tbody> @@ -149,7 +190,7 @@ </div> </div> </div> - {% endif %} + {#{% endif %}#} {% endif %} {% endblock %} {% block script %} @@ -175,7 +216,6 @@ $('tbody tr:not(:Contains(\'' + filter_val + '\'))').hide(); } }); - // add event button labeled "clear" $('#filter_clear').click(function (event) { $('#filter_input').val(''); diff --git a/networks/views.py b/networks/views.py index 24d6bc6..ee61043 100644 --- a/networks/views.py +++ b/networks/views.py @@ -8,6 +8,7 @@ from networks.forms import AddNetPool from vrtManager.network import wvmNetwork, wvmNetworks from vrtManager.network import network_size from libvirt import libvirtError +from django.contrib import messages @login_required @@ -121,6 +122,33 @@ def network(request, compute_id, pool): return HttpResponseRedirect(request.get_full_path()) except libvirtError as lib_err: error_messages.append(lib_err.message) + if 'modify_fixed_address' in request.POST: + name = request.POST.get('name', '') + address = request.POST.get('address', '') + mac = request.POST.get('mac', '') + try: + ret_val = conn.modify_fixed_address(name, address, mac) + messages.success(request, "Fixed Address Operation Completed.") + return HttpResponseRedirect(request.get_full_path()) + except libvirtError as lib_err: + error_messages.append(lib_err.message) + except ValueError as val_err: + error_messages.append(val_err.message) + if 'delete_fixed_address' in request.POST: + mac = request.POST.get('mac', '') + conn.delete_fixed_address(mac) + messages.success(request, "Fixed Address is Deleted.") + return HttpResponseRedirect(request.get_full_path()) + + if 'modify_dhcp_range' in request.POST: + range_start = request.POST.get('range_start', '') + range_end = request.POST.get('range_end', '') + try: + conn.modify_dhcp_range(range_start, range_end) + messages.success(request, "DHCP Range is Changed.") + return HttpResponseRedirect(request.get_full_path()) + except libvirtError as lib_err: + error_messages.append(lib_err.message) conn.close() diff --git a/static/css/webvirtcloud.css b/static/css/webvirtcloud.css index 542e8b9..2149dea 100644 --- a/static/css/webvirtcloud.css +++ b/static/css/webvirtcloud.css @@ -133,3 +133,21 @@ p { display: inline; min-width: 250px; } + +.multiselect { + width:27em; + height:10em; + border:solid 2px #c0c0c0; + overflow:auto; + padding: 0 1em; + margin: 0.9em; +} + +.multiselect label { + display:block; +} + +.multiselect-on { + color:#ffffff; + background-color:#000099; +} \ No newline at end of file diff --git a/vrtManager/IPy.py b/vrtManager/IPy.py index 393e468..1dba7b2 100644 --- a/vrtManager/IPy.py +++ b/vrtManager/IPy.py @@ -3,15 +3,14 @@ IPy - class and tools for handling of IPv4 and IPv6 addresses and networks. See README file for learn how to use IPy. Further Information might be available at: -http://software.inl.fr/trac/trac.cgi/wiki/IPy +https://github.com/haypo/python-ipy """ -# $HeadURL: https://svn.inl.fr/inl-svn/src/tools/ipy/tags/IPy-0.70/IPy.py $ -# $Id: IPy.py 19309 2009-10-29 10:21:13Z haypo $ - -__rcsid__ = '$Id: IPy.py 19309 2009-10-29 10:21:13Z haypo $' -__version__ = '0.70' +__version__ = '0.83' +import bisect +import collections +import sys import types # Definition of the Ranges for IPv4 IPs @@ -21,58 +20,117 @@ IPv4ranges = { '0': 'PUBLIC', # fall back '00000000': 'PRIVATE', # 0/8 '00001010': 'PRIVATE', # 10/8 + '0110010001': 'CARRIER_GRADE_NAT', # 100.64/10 '01111111': 'PRIVATE', # 127.0/8 '1': 'PUBLIC', # fall back '1010100111111110': 'PRIVATE', # 169.254/16 '101011000001': 'PRIVATE', # 172.16/12 '1100000010101000': 'PRIVATE', # 192.168/16 - '11011111': 'RESERVED', # 223/8 - '111': 'RESERVED' # 224/3 + '111': 'RESERVED', # 224/3 } # Definition of the Ranges for IPv6 IPs -# see also www.iana.org/assignments/ipv6-address-space, -# www.iana.org/assignments/ipv6-tla-assignments, -# www.iana.org/assignments/ipv6-multicast-addresses, -# www.iana.org/assignments/ipv6-anycast-addresses +# http://www.iana.org/assignments/ipv6-address-space/ +# http://www.iana.org/assignments/ipv6-unicast-address-assignments/ +# http://www.iana.org/assignments/ipv6-multicast-addresses/ IPv6ranges = { '00000000': 'RESERVED', # ::/8 - '00000001': 'UNASSIGNED', # 100::/8 - '0000001': 'NSAP', # 200::/7 - '0000010': 'IPX', # 400::/7 - '0000011': 'UNASSIGNED', # 600::/7 - '00001': 'UNASSIGNED', # 800::/5 - '0001': 'UNASSIGNED', # 1000::/4 - '0010000000000000': 'RESERVED', # 2000::/16 Reserved - '0010000000000001': 'ASSIGNABLE', # 2001::/16 Sub-TLA Assignments [RFC2450] - '00100000000000010000000': 'ASSIGNABLE IANA', # 2001:0000::/29 - 2001:01F8::/29 IANA - '00100000000000010000001': 'ASSIGNABLE APNIC', # 2001:0200::/29 - 2001:03F8::/29 APNIC - '00100000000000010000010': 'ASSIGNABLE ARIN', # 2001:0400::/29 - 2001:05F8::/29 ARIN - '00100000000000010000011': 'ASSIGNABLE RIPE', # 2001:0600::/29 - 2001:07F8::/29 RIPE NCC - '0010000000000010': '6TO4', # 2002::/16 "6to4" [RFC3056] - '0011111111111110': '6BONE', # 3FFE::/16 6bone Testing [RFC2471] - '0011111111111111': 'RESERVED', # 3FFF::/16 Reserved - '010': 'GLOBAL-UNICAST', # 4000::/3 - '011': 'UNASSIGNED', # 6000::/3 - '100': 'GEO-UNICAST', # 8000::/3 - '101': 'UNASSIGNED', # A000::/3 - '110': 'UNASSIGNED', # C000::/3 - '1110': 'UNASSIGNED', # E000::/4 - '11110': 'UNASSIGNED', # F000::/5 - '111110': 'UNASSIGNED', # F800::/6 - '1111110': 'UNASSIGNED', # FC00::/7 - '111111100': 'UNASSIGNED', # FE00::/9 - '1111111010': 'LINKLOCAL', # FE80::/10 - '1111111011': 'SITELOCAL', # FEC0::/10 - '11111111': 'MULTICAST', # FF00::/8 - '0' * 96: 'IPV4COMP', # ::/96 - '0' * 80 + '1' * 16: 'IPV4MAP', # ::FFFF:0:0/96 + '0' * 96: 'RESERVED', # ::/96 Formerly IPV4COMP [RFC4291] '0' * 128: 'UNSPECIFIED', # ::/128 - '0' * 127 + '1': 'LOOPBACK' # ::1/128 + '0' * 127 + '1': 'LOOPBACK', # ::1/128 + '0' * 80 + '1' * 16: 'IPV4MAP', # ::ffff:0:0/96 + '00000000011001001111111110011011' + '0' * 64: 'WKP46TRANS', # 0064:ff9b::/96 Well-Known-Prefix [RFC6052] + '00000001': 'UNASSIGNED', # 0100::/8 + '0000001': 'RESERVED', # 0200::/7 Formerly NSAP [RFC4048] + '0000010': 'RESERVED', # 0400::/7 Formerly IPX [RFC3513] + '0000011': 'RESERVED', # 0600::/7 + '00001': 'RESERVED', # 0800::/5 + '0001': 'RESERVED', # 1000::/4 + '001': 'GLOBAL-UNICAST', # 2000::/3 [RFC4291] + '00100000000000010000000': 'SPECIALPURPOSE', # 2001::/23 [RFC4773] + '00100000000000010000000000000000': 'TEREDO', # 2001::/32 [RFC4380] + '00100000000000010000000000000010' + '0' * 16: 'BMWG', # 2001:0002::/48 Benchmarking [RFC5180] + '0010000000000001000000000001': 'ORCHID', # 2001:0010::/28 (Temp until 2014-03-21) [RFC4843] + '00100000000000010000001': 'ALLOCATED APNIC', # 2001:0200::/23 + '00100000000000010000010': 'ALLOCATED ARIN', # 2001:0400::/23 + '00100000000000010000011': 'ALLOCATED RIPE NCC', # 2001:0600::/23 + '00100000000000010000100': 'ALLOCATED RIPE NCC', # 2001:0800::/23 + '00100000000000010000101': 'ALLOCATED RIPE NCC', # 2001:0a00::/23 + '00100000000000010000110': 'ALLOCATED APNIC', # 2001:0c00::/23 + '00100000000000010000110110111000': 'DOCUMENTATION', # 2001:0db8::/32 [RFC3849] + '00100000000000010000111': 'ALLOCATED APNIC', # 2001:0e00::/23 + '00100000000000010001001': 'ALLOCATED LACNIC', # 2001:1200::/23 + '00100000000000010001010': 'ALLOCATED RIPE NCC', # 2001:1400::/23 + '00100000000000010001011': 'ALLOCATED RIPE NCC', # 2001:1600::/23 + '00100000000000010001100': 'ALLOCATED ARIN', # 2001:1800::/23 + '00100000000000010001101': 'ALLOCATED RIPE NCC', # 2001:1a00::/23 + '0010000000000001000111': 'ALLOCATED RIPE NCC', # 2001:1c00::/22 + '00100000000000010010': 'ALLOCATED RIPE NCC', # 2001:2000::/20 + '001000000000000100110': 'ALLOCATED RIPE NCC', # 2001:3000::/21 + '0010000000000001001110': 'ALLOCATED RIPE NCC', # 2001:3800::/22 + '0010000000000001001111': 'RESERVED', # 2001:3c00::/22 Possible future allocation to RIPE NCC + '00100000000000010100000': 'ALLOCATED RIPE NCC', # 2001:4000::/23 + '00100000000000010100001': 'ALLOCATED AFRINIC', # 2001:4200::/23 + '00100000000000010100010': 'ALLOCATED APNIC', # 2001:4400::/23 + '00100000000000010100011': 'ALLOCATED RIPE NCC', # 2001:4600::/23 + '00100000000000010100100': 'ALLOCATED ARIN', # 2001:4800::/23 + '00100000000000010100101': 'ALLOCATED RIPE NCC', # 2001:4a00::/23 + '00100000000000010100110': 'ALLOCATED RIPE NCC', # 2001:4c00::/23 + '00100000000000010101': 'ALLOCATED RIPE NCC', # 2001:5000::/20 + '0010000000000001100': 'ALLOCATED APNIC', # 2001:8000::/19 + '00100000000000011010': 'ALLOCATED APNIC', # 2001:a000::/20 + '00100000000000011011': 'ALLOCATED APNIC', # 2001:b000::/20 + '0010000000000010': '6TO4', # 2002::/16 "6to4" [RFC3056] + '001000000000001100': 'ALLOCATED RIPE NCC', # 2003::/18 + '001001000000': 'ALLOCATED APNIC', # 2400::/12 + '001001100000': 'ALLOCATED ARIN', # 2600::/12 + '00100110000100000000000': 'ALLOCATED ARIN', # 2610::/23 + '00100110001000000000000': 'ALLOCATED ARIN', # 2620::/23 + '001010000000': 'ALLOCATED LACNIC', # 2800::/12 + '001010100000': 'ALLOCATED RIPE NCC', # 2a00::/12 + '001011000000': 'ALLOCATED AFRINIC', # 2c00::/12 + '00101101': 'RESERVED', # 2d00::/8 + '0010111': 'RESERVED', # 2e00::/7 + '0011': 'RESERVED', # 3000::/4 + '010': 'RESERVED', # 4000::/3 + '011': 'RESERVED', # 6000::/3 + '100': 'RESERVED', # 8000::/3 + '101': 'RESERVED', # a000::/3 + '110': 'RESERVED', # c000::/3 + '1110': 'RESERVED', # e000::/4 + '11110': 'RESERVED', # f000::/5 + '111110': 'RESERVED', # f800::/6 + '1111110': 'ULA', # fc00::/7 [RFC4193] + '111111100': 'RESERVED', # fe00::/9 + '1111111010': 'LINKLOCAL', # fe80::/10 + '1111111011': 'RESERVED', # fec0::/10 Formerly SITELOCAL [RFC4291] + '11111111': 'MULTICAST', # ff00::/8 + '1111111100000001': 'NODE-LOCAL MULTICAST', # ff01::/16 + '1111111100000010': 'LINK-LOCAL MULTICAST', # ff02::/16 + '1111111100000100': 'ADMIN-LOCAL MULTICAST', # ff04::/16 + '1111111100000101': 'SITE-LOCAL MULTICAST', # ff05::/16 + '1111111100001000': 'ORG-LOCAL MULTICAST', # ff08::/16 + '1111111100001110': 'GLOBAL MULTICAST', # ff0e::/16 + '1111111100001111': 'RESERVED MULTICAST', # ff0f::/16 + '111111110011': 'PREFIX-BASED MULTICAST', # ff30::/12 [RFC3306] + '111111110111': 'RP-EMBEDDED MULTICAST', # ff70::/12 [RFC3956] } +MAX_IPV4_ADDRESS = 0xffffffff +MAX_IPV6_ADDRESS = 0xffffffffffffffffffffffffffffffff +IPV6_TEST_MAP = 0xffffffffffffffffffffffff00000000 +IPV6_MAP_MASK = 0x00000000000000000000ffff00000000 -class IPint: +if sys.version_info >= (3,): + INT_TYPES = (int,) + STR_TYPES = (str,) + xrange = range +else: + INT_TYPES = (int, long) + STR_TYPES = (str, unicode) + + +class IPint(object): """Handling of IP addresses returning integers. Use class IP instead because some features are not implemented for @@ -95,13 +153,13 @@ class IPint: If make_net is True, an IP address will be transformed into the network address by applying the specified netmask. - >>> print IP('127.0.0.0/8') + >>> print(IP('127.0.0.0/8')) 127.0.0.0/8 - >>> print IP('127.0.0.0/255.0.0.0') + >>> print(IP('127.0.0.0/255.0.0.0')) 127.0.0.0/8 - >>> print IP('127.0.0.0-127.255.255.255') + >>> print(IP('127.0.0.0-127.255.255.255')) 127.0.0.0/8 - >>> print IP('127.0.0.1/255.0.0.0', make_net=True) + >>> print(IP('127.0.0.1/255.0.0.0', make_net=True)) 127.0.0.0/8 See module documentation for more examples. @@ -117,19 +175,23 @@ class IPint: prefixlen = -1 # handling of non string values in constructor - if type(data) == types.IntType or type(data) == types.LongType: - self.ip = long(data) + if isinstance(data, INT_TYPES): + self.ip = int(data) if ipversion == 0: - if self.ip < 0x100000000L: + if self.ip <= MAX_IPV4_ADDRESS: ipversion = 4 else: ipversion = 6 if ipversion == 4: + if self.ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip)) prefixlen = 32 elif ipversion == 6: + if self.ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, self.ip)) prefixlen = 128 else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") self._ipversion = ipversion self._prefixlen = prefixlen # handle IP instance as an parameter @@ -137,7 +199,7 @@ class IPint: self._ipversion = data._ipversion self._prefixlen = data._prefixlen self.ip = data.ip - else: + elif isinstance(data, STR_TYPES): # TODO: refactor me! # splitting of a string into IP and prefixlen et. al. x = data.split('-') @@ -146,20 +208,19 @@ class IPint: (ip, last) = x (self.ip, parsedVersion) = parseAddress(ip) if parsedVersion != 4: - raise ValueError, "first-last notation only allowed for IPv4" + raise ValueError("first-last notation only allowed for IPv4") (last, lastversion) = parseAddress(last) if lastversion != 4: - raise ValueError, "last address should be IPv4, too" + raise ValueError("last address should be IPv4, too") if last < self.ip: - raise ValueError, "last address should be larger than first" + raise ValueError("last address should be larger than first") size = last - self.ip netbits = _count1Bits(size) # make sure the broadcast is the same as the last ip # otherwise it will return /16 for something like: # 192.168.0.0-192.168.191.255 if IP('%s/%s' % (ip, 32 - netbits)).broadcast().int() != last: - raise ValueError, \ - "the range %s is not on a network boundary." % data + raise ValueError("the range %s is not on a network boundary." % data) elif len(x) == 1: x = data.split('/') # if no prefix is given use defaults @@ -167,7 +228,7 @@ class IPint: ip = x[0] prefixlen = -1 elif len(x) > 2: - raise ValueError, "only one '/' allowed in IP Address" + raise ValueError("only one '/' allowed in IP Address") else: (ip, prefixlen) = x if prefixlen.find('.') != -1: @@ -175,23 +236,19 @@ class IPint: # a.b.c.d/255.255.255.0 (netmask, vers) = parseAddress(prefixlen) if vers != 4: - raise ValueError, "netmask must be IPv4" + raise ValueError("netmask must be IPv4") prefixlen = _netmaskToPrefixlen(netmask) elif len(x) > 2: - raise ValueError, "only one '-' allowed in IP Address" + raise ValueError("only one '-' allowed in IP Address") else: - raise ValueError, "can't parse" + raise ValueError("can't parse") (self.ip, parsedVersion) = parseAddress(ip) if ipversion == 0: ipversion = parsedVersion if prefixlen == -1: - if ipversion == 4: - prefixlen = 32 - netbits - elif ipversion == 6: - prefixlen = 128 - netbits - else: - raise ValueError, "only IPv4 and IPv6 supported" + bits = _ipVersionToLen(ipversion) + prefixlen = bits - netbits self._ipversion = ipversion self._prefixlen = int(prefixlen) @@ -200,7 +257,9 @@ class IPint: if not _checkNetaddrWorksWithPrefixlen(self.ip, self._prefixlen, self._ipversion): - raise ValueError, "%s has invalid prefix length (%s)" % (repr(self), self._prefixlen) + raise ValueError("%s has invalid prefix length (%s)" % (repr(self), self._prefixlen)) + else: + raise TypeError("Unsupported data type: %s" % type(data)) def int(self): """Return the first / base / network addess as an (long) integer. @@ -267,8 +326,7 @@ class IPint: if want == 2: # this should work with IP and IPint netmask = self.netmask() - if type(netmask) != types.IntType \ - and type(netmask) != types.LongType: + if not isinstance(netmask, INT_TYPES): netmask = netmask.int() return "/%s" % (intToIp(netmask, self._ipversion)) elif want == 3: @@ -279,27 +337,23 @@ class IPint: else: return '' - # We have different flavours to convert to: - # strFullsize 127.0.0.1 2001:0658:022a:cafe:0200:c0ff:fe8d:08fa - # strNormal 127.0.0.1 2001:658:22a:cafe:200:c0ff:fe8d:08fa - # strCompressed 127.0.0.1 2001:658:22a:cafe::1 - # strHex 0x7F000001L 0x20010658022ACAFE0200C0FFFE8D08FA - # strDec 2130706433 42540616829182469433547974687817795834 + # We have different flavours to convert to: + # strFullsize 127.0.0.1 2001:0658:022a:cafe:0200:c0ff:fe8d:08fa + # strNormal 127.0.0.1 2001:658:22a:cafe:200:c0ff:fe8d:08fa + # strCompressed 127.0.0.1 2001:658:22a:cafe::1 + # strHex 0x7F000001 0x20010658022ACAFE0200C0FFFE8D08FA + # strDec 2130706433 42540616829182469433547974687817795834 def strBin(self, wantprefixlen=None): """Return a string representation as a binary value. - >>> print IP('127.0.0.1').strBin() + >>> print(IP('127.0.0.1').strBin()) 01111111000000000000000000000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strBin()) + 00100000000000010000011001011000000000100010101011001010111111100000001000000000000000000000000000000000000000000000000000000001 """ - if self._ipversion == 4: - bits = 32 - elif self._ipversion == 6: - bits = 128 - else: - raise ValueError, "only IPv4 and IPv6 supported" - + bits = _ipVersionToLen(self._ipversion) if self.WantPrefixLen == None and wantprefixlen == None: wantprefixlen = 0 ret = _intToBin(self.ip) @@ -323,7 +377,7 @@ class IPint: return self.strFullsize(wantprefixlen) else: if self.ip >> 32 == 0xffff: - ipv4 = intToIp(self.ip & 0xffffffff, 4) + ipv4 = intToIp(self.ip & MAX_IPV4_ADDRESS, 4) text = "::ffff:" + ipv4 + self._printPrefix(wantprefixlen) return text # find the longest sequence of '0' @@ -331,7 +385,7 @@ class IPint: # every element of followingzeros will contain the number of zeros # following the corresponding element of hextets followingzeros = [0] * 8 - for i in range(len(hextets)): + for i in xrange(len(hextets)): followingzeros[i] = _countFollowingZeros(hextets[i:]) # compressionpos is the position where we can start removing zeros compressionpos = followingzeros.index(max(followingzeros)) @@ -353,9 +407,9 @@ class IPint: def strNormal(self, wantprefixlen=None): """Return a string representation in the usual format. - >>> print IP('127.0.0.1').strNormal() + >>> print(IP('127.0.0.1').strNormal()) 127.0.0.1 - >>> print IP('2001:0658:022a:cafe:0200::1').strNormal() + >>> print(IP('2001:0658:022a:cafe:0200::1').strNormal()) 2001:658:22a:cafe:200:0:0:1 """ @@ -365,73 +419,69 @@ class IPint: if self._ipversion == 4: ret = self.strFullsize(0) elif self._ipversion == 6: - ret = ':'.join([hex(x)[2:] for x in [int(x, 16) for x in self.strFullsize(0).split(':')]]) + ret = ':'.join(["%x" % x for x in [int(x, 16) for x in self.strFullsize(0).split(':')]]) else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") return ret + self._printPrefix(wantprefixlen) def strFullsize(self, wantprefixlen=None): """Return a string representation in the non-mangled format. - >>> print IP('127.0.0.1').strFullsize() + >>> print(IP('127.0.0.1').strFullsize()) 127.0.0.1 - >>> print IP('2001:0658:022a:cafe:0200::1').strFullsize() + >>> print(IP('2001:0658:022a:cafe:0200::1').strFullsize()) 2001:0658:022a:cafe:0200:0000:0000:0001 """ if self.WantPrefixLen == None and wantprefixlen == None: wantprefixlen = 1 - return intToIp(self.ip, self._ipversion).lower() + self._printPrefix(wantprefixlen) + return intToIp(self.ip, self._ipversion) + self._printPrefix(wantprefixlen) def strHex(self, wantprefixlen=None): """Return a string representation in hex format in lower case. - >>> IP('127.0.0.1').strHex() - '0x7f000001' - >>> IP('2001:0658:022a:cafe:0200::1').strHex() - '0x20010658022acafe0200000000000001' + >>> print(IP('127.0.0.1').strHex()) + 0x7f000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strHex()) + 0x20010658022acafe0200000000000001 """ if self.WantPrefixLen == None and wantprefixlen == None: wantprefixlen = 0 - x = hex(self.ip) - if x[-1] == 'L': - x = x[:-1] - return x.lower() + self._printPrefix(wantprefixlen) + x = '0x%x' % self.ip + return x + self._printPrefix(wantprefixlen) def strDec(self, wantprefixlen=None): """Return a string representation in decimal format. - >>> print IP('127.0.0.1').strDec() + >>> print(IP('127.0.0.1').strDec()) 2130706433 - >>> print IP('2001:0658:022a:cafe:0200::1').strDec() + >>> print(IP('2001:0658:022a:cafe:0200::1').strDec()) 42540616829182469433547762482097946625 """ if self.WantPrefixLen == None and wantprefixlen == None: wantprefixlen = 0 - x = str(self.ip) - if x[-1] == 'L': - x = x[:-1] + x = '%d' % self.ip return x + self._printPrefix(wantprefixlen) def iptype(self): - """Return a description of the IP type ('PRIVATE', 'RESERVERD', etc). + """Return a description of the IP type ('PRIVATE', 'RESERVED', etc). - >>> print IP('127.0.0.1').iptype() + >>> print(IP('127.0.0.1').iptype()) PRIVATE - >>> print IP('192.168.1.1').iptype() + >>> print(IP('192.168.1.1').iptype()) PRIVATE - >>> print IP('195.185.1.2').iptype() + >>> print(IP('195.185.1.2').iptype()) PUBLIC - >>> print IP('::1').iptype() + >>> print(IP('::1').iptype()) LOOPBACK - >>> print IP('2001:0658:022a:cafe:0200::1').iptype() - ASSIGNABLE RIPE + >>> print(IP('2001:0658:022a:cafe:0200::1').iptype()) + ALLOCATED RIPE NCC The type information for IPv6 is out of sync with reality. """ @@ -443,15 +493,14 @@ class IPint: elif self._ipversion == 6: iprange = IPv6ranges else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") bits = self.strBin() - for i in range(len(bits), 0, -1): - if iprange.has_key(bits[:i]): + for i in xrange(len(bits), 0, -1): + if bits[:i] in iprange: return iprange[bits[:i]] return "unknown" - def netmask(self): """Return netmask as an integer. @@ -460,53 +509,41 @@ class IPint: """ # TODO: unify with prefixlenToNetmask? - if self._ipversion == 4: - locallen = 32 - self._prefixlen - elif self._ipversion == 6: - locallen = 128 - self._prefixlen - else: - raise ValueError, "only IPv4 and IPv6 supported" - - return ((2L ** self._prefixlen) - 1) << locallen + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + return ((2 ** self._prefixlen) - 1) << locallen def strNetmask(self): """Return netmask as an string. Mostly useful for IPv6. - >>> print IP('195.185.0.0/16').strNetmask() + >>> print(IP('195.185.0.0/16').strNetmask()) 255.255.0.0 - >>> print IP('2001:0658:022a:cafe::0/64').strNetmask() + >>> print(IP('2001:0658:022a:cafe::0/64').strNetmask()) /64 """ # TODO: unify with prefixlenToNetmask? + # Note: call to _ipVersionToLen() also validates version is 4 or 6 + bits = _ipVersionToLen(self._ipversion) if self._ipversion == 4: - locallen = 32 - self._prefixlen - return intToIp(((2L ** self._prefixlen) - 1) << locallen, 4) + locallen = bits - self._prefixlen + return intToIp(((2 ** self._prefixlen) - 1) << locallen, 4) elif self._ipversion == 6: - locallen = 128 - self._prefixlen return "/%d" % self._prefixlen - else: - raise ValueError, "only IPv4 and IPv6 supported" def len(self): """Return the length of a subnet. - >>> print IP('195.185.1.0/28').len() + >>> print(IP('195.185.1.0/28').len()) 16 - >>> print IP('195.185.1.0/24').len() + >>> print(IP('195.185.1.0/24').len()) 256 """ - if self._ipversion == 4: - locallen = 32 - self._prefixlen - elif self._ipversion == 6: - locallen = 128 - self._prefixlen - else: - raise ValueError, "only IPv4 and IPv6 supported" - - return 2L ** locallen - + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + return 2 ** locallen def __nonzero__(self): """All IPy objects should evaluate to true in boolean context. @@ -514,27 +551,49 @@ class IPint: 0.0.0.0/0, the __len__() of the object becomes 0, which is used as the boolean value of the object. """ - return 1 - + return True def __len__(self): - """Return the length of a subnet. + """ + Return the length of a subnet. Called to implement the built-in function len(). - It breaks with IPv6 Networks. Anybody knows how to fix this.""" + It will break with large IPv6 Networks. + Use the object's len() instead. + """ + return self.len() - # Python < 2.2 has this silly restriction which breaks IPv6 - # how about Python >= 2.2 ... ouch - it persists! - - return int(self.len()) + def __add__(self, other): + """Emulate numeric objects through network aggregation""" + if self._ipversion != other._ipversion: + raise ValueError("Only networks with the same IP version can be added.") + if self._prefixlen != other._prefixlen: + raise ValueError("Only networks with the same prefixlen can be added.") + if self._prefixlen < 1: + raise ValueError("Networks with a prefixlen longer than /1 can't be added.") + if self > other: + # fixed by Skinny Puppy <skin_pup-IPy@happypoo.com> + return other.__add__(self) + if other.int() - self[-1].int() != 1: + raise ValueError("Only adjacent networks can be added together.") + ret = IP(self.int(), ipversion=self._ipversion) + ret._prefixlen = self.prefixlen() - 1 + if not _checkNetaddrWorksWithPrefixlen(ret.ip, ret._prefixlen, + ret._ipversion): + raise ValueError("The resulting %s has invalid prefix length (%s)" + % (repr(ret), ret._prefixlen)) + return ret + def __sub__(self, other): + """Return the prefixes that are in this IP but not in the other""" + return _remove_subprefix(self, other) def __getitem__(self, key): """Called to implement evaluation of self[key]. >>> ip=IP('127.0.0.0/30') >>> for x in ip: - ... print repr(x) + ... print(repr(x)) ... IP('127.0.0.0') IP('127.0.0.1') @@ -546,7 +605,9 @@ class IPint: IP('127.0.0.3') """ - if type(key) != types.IntType and type(key) != types.LongType: + if isinstance(key, slice): + return [self.ip + int(x) for x in xrange(*key.indices(len(self)))] + if not isinstance(key, INT_TYPES): raise TypeError if key < 0: if abs(key) <= self.len(): @@ -557,8 +618,7 @@ class IPint: if key >= self.len(): raise IndexError - return self.ip + long(key) - + return self.ip + int(key) def __contains__(self, item): """Called to implement membership test operators. @@ -568,20 +628,23 @@ class IPint: >>> IP('195.185.1.1').strHex() '0xc3b90101' - >>> 0xC3B90101L in IP('195.185.1.0/24') - 1 + >>> 0xC3B90101 in IP('195.185.1.0/24') + True >>> '127.0.0.1' in IP('127.0.0.0/24') - 1 + True >>> IP('127.0.0.0/24') in IP('127.0.0.0/25') - 0 + False """ - item = IP(item) - if item.ip >= self.ip and item.ip < self.ip + self.len() - item.len() + 1: - return 1 + if isinstance(item, IP): + if item._ipversion != self._ipversion: + return False else: - return 0 - + item = IP(item) + if item.ip >= self.ip and item.ip < self.ip + self.len() - item.len() + 1: + return True + else: + return False def overlaps(self, item): """Check if two IP address ranges overlap. @@ -599,7 +662,8 @@ class IPint: -1 """ - item = IP(item) + if not isinstance(item, IP): + item = IP(item) if item.ip >= self.ip and item.ip < self.ip + self.len(): return 1 elif self.ip >= item.ip and self.ip < item.ip + item.len(): @@ -607,7 +671,6 @@ class IPint: else: return 0 - def __str__(self): """Dispatch to the prefered String Representation. @@ -615,7 +678,6 @@ class IPint: return self.strCompressed() - def __repr__(self): """Print a representation of the Object. @@ -623,69 +685,77 @@ class IPint: to an identical Object (without the wantprefixlen stuff - see module docstring. - >>> print repr(IP('10.0.0.0/24')) + >>> print(repr(IP('10.0.0.0/24'))) IP('10.0.0.0/24') """ return ("IPint('%s')" % (self.strCompressed(1))) - def __cmp__(self, other): """Called by comparison operations. Should return a negative integer if self < other, zero if self == other, a positive integer if self > other. - Networks with different prefixlen are considered non-equal. - Networks with the same prefixlen and differing addresses are - considered non equal but are compared by their base address - integer value to aid sorting of IP objects. + Order is first determined by the address family. IPv4 addresses + are always smaller than IPv6 addresses: - The version of Objects is not put into consideration. + >>> IP('10.0.0.0') < IP('2001:db8::') + 1 + + Then the first address is compared. Lower addresses are + always smaller: + + >>> IP('10.0.0.0') > IP('10.0.0.1') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.1') + 0 + >>> IP('10.0.1.0') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0') + 1 + + Then the prefix length is compared. Shorter prefixes are + considered smaller than longer prefixes: >>> IP('10.0.0.0/24') > IP('10.0.0.0') - 1 - >>> IP('10.0.0.0/24') < IP('10.0.0.0') 0 - >>> IP('10.0.0.0/24') < IP('12.0.0.0/24') - 1 - >>> IP('10.0.0.0/24') > IP('12.0.0.0/24') + >>> IP('10.0.0.0/24') > IP('10.0.0.0/25') 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.0/23') + 1 """ + if not isinstance(other, IPint): + raise TypeError - # Im not really sure if this is "the right thing to do" - if self._prefixlen < other.prefixlen(): - return (other.prefixlen() - self._prefixlen) - elif self._prefixlen > other.prefixlen(): + # Lower version -> lower result + if self._ipversion != other._ipversion: + return self._ipversion < other._ipversion and -1 or 1 - # Fixed bySamuel Krempp <krempp@crans.ens-cachan.fr>: + # Lower start address -> lower result + if self.ip != other.ip: + return self.ip < other.ip and -1 or 1 - # The bug is quite obvious really (as 99% bugs are once - # spotted, isn't it ? ;-) Because of precedence of - # multiplication by -1 over the substraction, prefixlen - # differences were causing the __cmp__ function to always - # return positive numbers, thus the function was failing - # the basic assumptions for a __cmp__ function. + # Shorter prefix length -> lower result + if self._prefixlen != other._prefixlen: + return self._prefixlen < other._prefixlen and -1 or 1 - # Namely we could have (a > b AND b > a), when the - # prefixlen of a and b are different. (eg let - # a=IP("1.0.0.0/24"); b=IP("2.0.0.0/16");) thus, anything - # could happen when launching a sort algorithm.. - # everything's in order with the trivial, attached patch. + # No differences found + return 0 - return (self._prefixlen - other.prefixlen()) * -1 - else: - if self.ip < other.ip: - return -1 - elif self.ip > other.ip: - return 1 - elif self._ipversion != other._ipversion: - # IP('0.0.0.0'), IP('::/0') - return cmp(self._ipversion, other._ipversion) - else: - return 0 + def __eq__(self, other): + if not isinstance(other, IPint): + return False + return self.__cmp__(other) == 0 + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.__cmp__(other) < 0 def __hash__(self): """Called for the key object for dictionary operations, and by @@ -736,8 +806,17 @@ class IP(IPint): >>> IP('10.0.0.0/8').netmask() IP('255.0.0.0') """ - return IP(IPint.netmask(self)) + return IP(IPint.netmask(self), ipversion=self._ipversion) + def _getIPv4Map(self): + if self._ipversion != 6: + return None + if (self.ip >> 32) != 0xffff: + return None + ipv4 = self.ip & MAX_IPV4_ADDRESS + if self._prefixlen != 128: + ipv4 = '%s/%s' % (ipv4, 32 - (128 - self._prefixlen)) + return IP(ipv4, ipversion=4) def reverseNames(self): """Return a list with values forming the reverse lookup. @@ -768,30 +847,30 @@ class IP(IPint): if self.len() < 2 ** 8: for x in self: ret.append(x.reverseName()) - elif self.len() < 2 ** 16L: - for i in range(0, self.len(), 2 ** 8): + elif self.len() < 2 ** 16: + for i in xrange(0, self.len(), 2 ** 8): ret.append(self[i].reverseName()[2:]) - elif self.len() < 2 ** 24L: - for i in range(0, self.len(), 2 ** 16): + elif self.len() < 2 ** 24: + for i in xrange(0, self.len(), 2 ** 16): ret.append(self[i].reverseName()[4:]) else: - for i in range(0, self.len(), 2 ** 24): + for i in xrange(0, self.len(), 2 ** 24): ret.append(self[i].reverseName()[6:]) return ret elif self._ipversion == 6: - s = hex(self.ip)[2:].lower() - if s[-1] == 'l': - s = s[:-1] + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseNames() + s = "%x" % self.ip if self._prefixlen % 4 != 0: - raise NotImplementedError, "can't create IPv6 reverse names at sub nibble level" + raise NotImplementedError("can't create IPv6 reverse names at sub nibble level") s = list(s) s.reverse() s = '.'.join(s) - first_nibble_index = int(32 - (self._prefixlen / 4)) * 2 + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 return ["%s.ip6.arpa." % s[first_nibble_index:]] else: - raise ValueError, "only IPv4 and IPv6 supported" - + raise ValueError("only IPv4 and IPv6 supported") def reverseName(self): """Return the value for reverse lookup/PTR records as RFC 2317 look alike. @@ -800,24 +879,24 @@ class IP(IPint): for /23. Do not use it. Better set up a zone for every address. See reverseName for a way to achieve that. - >>> print IP('195.185.1.1').reverseName() + >>> print(IP('195.185.1.1').reverseName()) 1.1.185.195.in-addr.arpa. - >>> print IP('195.185.1.0/28').reverseName() + >>> print(IP('195.185.1.0/28').reverseName()) 0-15.1.185.195.in-addr.arpa. >>> IP('::1:2').reverseName() - '2.0.0.0.1.ip6.arpa.' + '2.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.' + >>> IP('ff02::/64').reverseName() + '0.0.0.0.0.0.0.0.0.0.0.0.2.0.f.f.ip6.arpa.' """ if self._ipversion == 4: s = self.strFullsize(0) s = s.split('.') s.reverse() - first_byte_index = int(4 - (self._prefixlen / 8)) + first_byte_index = int(4 - (self._prefixlen // 8)) if self._prefixlen % 8 != 0: nibblepart = "%s-%s" % ( - s[3 - (self._prefixlen / 8)], intToIp(self.ip + self.len() - 1, 4).split('.')[-1]) - if nibblepart[-1] == 'l': - nibblepart = nibblepart[:-1] + s[3 - (self._prefixlen // 8)], intToIp(self.ip + self.len() - 1, 4).split('.')[-1]) nibblepart += '.' else: nibblepart = "" @@ -826,23 +905,22 @@ class IP(IPint): return "%s%s.in-addr.arpa." % (nibblepart, s) elif self._ipversion == 6: - s = hex(self.ip)[2:].lower() - if s[-1] == 'l': - s = s[:-1] + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseName() + s = '%032x' % self.ip if self._prefixlen % 4 != 0: - nibblepart = "%s-%s" % (s[self._prefixlen:], hex(self.ip + self.len() - 1)[2:].lower()) - if nibblepart[-1] == 'l': - nibblepart = nibblepart[:-1] + nibblepart = "%s-%x" % (s[self._prefixlen:], self.ip + self.len() - 1) nibblepart += '.' else: nibblepart = "" s = list(s) s.reverse() s = '.'.join(s) - first_nibble_index = int(32 - (self._prefixlen / 4)) * 2 + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 return "%s%s.ip6.arpa." % (nibblepart, s[first_nibble_index:]) else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") def make_net(self, netmask): """Transform a single IP address into a network specification by @@ -850,11 +928,11 @@ class IP(IPint): Returns a new IP instance. - >>> print IP('127.0.0.1').make_net('255.0.0.0') + >>> print(IP('127.0.0.1').make_net('255.0.0.0')) 127.0.0.0/8 """ if '/' in str(netmask): - raise ValueError, "invalid netmask (%s)" % netmask + raise ValueError("invalid netmask (%s)" % netmask) return IP('%s/%s' % (self, netmask), make_net=True) def __getitem__(self, key): @@ -862,18 +940,20 @@ class IP(IPint): >>> ip=IP('127.0.0.0/30') >>> for x in ip: - ... print str(x) + ... print(str(x)) ... 127.0.0.0 127.0.0.1 127.0.0.2 127.0.0.3 - >>> print str(ip[2]) + >>> print(str(ip[2])) 127.0.0.2 - >>> print str(ip[-1]) + >>> print(str(ip[-1])) 127.0.0.3 """ - return IP(IPint.__getitem__(self, key)) + if isinstance(key, slice): + return [IP(IPint.__getitem__(self, x), ipversion=self._ipversion) for x in xrange(*key.indices(len(self)))] + return IP(IPint.__getitem__(self, key), ipversion=self._ipversion) def __repr__(self): """Print a representation of the Object. @@ -884,76 +964,302 @@ class IP(IPint): return ("IP('%s')" % (self.strCompressed(1))) - def __add__(self, other): - """Emulate numeric objects through network aggregation""" - if self.prefixlen() != other.prefixlen(): - raise ValueError, "Only networks with the same prefixlen can be added." - if self.prefixlen < 1: - raise ValueError, "Networks with a prefixlen longer than /1 can't be added." - if self.version() != other.version(): - raise ValueError, "Only networks with the same IP version can be added." - if self > other: - # fixed by Skinny Puppy <skin_pup-IPy@happypoo.com> - return other.__add__(self) + def get_mac(self): + """ + Get the 802.3 MAC address from IPv6 RFC 2464 address, in lower case. + Return None if the address is an IPv4 or not a IPv6 RFC 2464 address. + + >>> IP('fe80::f66d:04ff:fe47:2fae').get_mac() + 'f4:6d:04:47:2f:ae' + """ + if self._ipversion != 6: + return None + if (self.ip & 0x20000ffff000000) != 0x20000fffe000000: + return None + return '%02x:%02x:%02x:%02x:%02x:%02x' % ( + (((self.ip >> 56) & 0xff) & 0xfd), + (self.ip >> 48) & 0xff, + (self.ip >> 40) & 0xff, + (self.ip >> 16) & 0xff, + (self.ip >> 8) & 0xff, + self.ip & 0xff, + ) + + def v46map(self): + """ + Returns the IPv6 mapped address of an IPv4 address, or the corresponding + IPv4 address if the IPv6 address is in the appropriate range. + Raises a ValueError if the IPv6 address is not translatable. See RFC 4291. + + >>> IP('192.168.1.1').v46map() + IP('::ffff:192.168.1.1') + >>> IP('::ffff:192.168.1.1').v46map() + IP('192.168.1.1') + """ + if self._ipversion == 4: + return IP(str(IPV6_MAP_MASK + self.ip) + + "/%s" % (self._prefixlen + 96)) else: - ret = IP(self.int()) - ret._prefixlen = self.prefixlen() - 1 - return ret + if self.ip & IPV6_TEST_MAP == IPV6_MAP_MASK: + return IP(str(self.ip - IPV6_MAP_MASK) + + "/%s" % (self._prefixlen - 96)) + raise ValueError("%s cannot be converted to an IPv4 address." + % repr(self)) + + +class IPSet(collections.MutableSet): + def __init__(self, iterable=[]): + # Make sure it's iterable, otherwise wrap + if not isinstance(iterable, collections.Iterable): + raise TypeError("'%s' object is not iterable" % type(iterable).__name__) + + # Make sure we only accept IP objects + for prefix in iterable: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Store and optimize + self.prefixes = iterable[:] + self.optimize() + + def __contains__(self, ip): + valid_masks = self.prefixtable.keys() + if isinstance(ip, IP): + # Don't dig through more-specific ranges + ip_mask = ip._prefixlen + valid_masks = [x for x in valid_masks if x <= ip_mask] + for mask in sorted(valid_masks): + i = bisect.bisect(self.prefixtable[mask], ip) + # Because of sorting order, a match can only occur in the prefix + # that comes before the result of the search. + if i and ip in self.prefixtable[mask][i - 1]: + return True + + def __iter__(self): + for prefix in self.prefixes: + yield prefix + + def __len__(self): + return self.len() + + def __add__(self, other): + return IPSet(self.prefixes + other.prefixes) + + def __sub__(self, other): + new = IPSet(self.prefixes) + for prefix in other: + new.discard(prefix) + return new + + def __and__(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + result = [] + try: + l = next(left) + r = next(right) + while True: + # iterate over prefixes in order, keeping the smaller of the + # two if they overlap + if l in r: + result.append(l) + l = next(left) + continue + elif r in l: + result.append(r) + r = next(right) + continue + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return IPSet(result) + + def __repr__(self): + return '%s([' % self.__class__.__name__ + ', '.join(map(repr, self.prefixes)) + '])' + + def len(self): + return sum(prefix.len() for prefix in self.prefixes) + + def add(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # Check type + for prefix in value: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Append and optimize + self.prefixes.extend(value) + self.optimize() + + def discard(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # This is much faster than iterating over the addresses + if isinstance(value, IPSet): + value = value.prefixes + + # Remove + for del_prefix in value: + if not isinstance(del_prefix, IP): + raise ValueError('Only IP objects can be removed from an IPSet') + + # First check if this prefix contains anything in our list + found = False + d = 0 + for i in range(len(self.prefixes)): + if self.prefixes[i - d] in del_prefix: + self.prefixes.pop(i - d) + d = d + 1 + found = True + + if found: + # If the prefix was bigger than an existing prefix, then it's + # certainly not a subset of one, so skip the rest + continue + + # Maybe one of our prefixes contains this prefix + found = False + for i in range(len(self.prefixes)): + if del_prefix in self.prefixes[i]: + self.prefixes[i:i + 1] = self.prefixes[i] - del_prefix + break + + self.optimize() + + def isdisjoint(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + try: + l = next(left) + r = next(right) + while True: + if l in r or r in l: + return False + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return True + + def optimize(self): + # The algorithm below *depends* on the sort order + self.prefixes.sort() + + # First eliminate all values that are a subset of other values + addrlen = len(self.prefixes) + i = 0 + while i < addrlen: + # Everything that might be inside this prefix follows + # directly behind it + j = i + 1 + while j < addrlen and self.prefixes[j] in self.prefixes[i]: + # Mark for deletion by overwriting with None + self.prefixes[j] = None + j += 1 + + # Continue where we left off + i = j + + # Try to merge as many prefixes as possible + run_again = True + while run_again: + # Filter None values. This happens when a subset is eliminated + # above, or when two prefixes are merged below + self.prefixes = [a for a in self.prefixes if a is not None] + + # We'll set run_again to True when we make changes that require + # re-evaluation of the whole list + run_again = False + + # We can merge two prefixes that have the same version, same + # prefix length and differ only on the last bit of the prefix + addrlen = len(self.prefixes) + i = 0 + while i < addrlen - 1: + j = i + 1 + + try: + # The next line will throw an exception when merging + # is not possible + self.prefixes[i] += self.prefixes[j] + self.prefixes[j] = None + i = j + 1 + run_again = True + except ValueError: + # Can't be merged, see if position j can be merged + i = j + + # O(n) insertion now by prefix means faster searching on __contains__ + # when lots of ranges with the same length exist + self.prefixtable = {} + for address in self.prefixes: + try: + self.prefixtable[address._prefixlen].append(address) + except KeyError: + self.prefixtable[address._prefixlen] = [address] def _parseAddressIPv6(ipstr): """ Internal function used by parseAddress() to parse IPv6 address with ':'. - >>> _parseAddressIPv6('::') - 0L - >>> _parseAddressIPv6('::1') - 1L - >>> _parseAddressIPv6('0:0:0:0:0:0:0:1') - 1L - >>> _parseAddressIPv6('0:0:0::0:0:1') - 1L - >>> _parseAddressIPv6('0:0:0:0:0:0:0:0') - 0L - >>> _parseAddressIPv6('0:0:0::0:0:0') - 0L + >>> print(_parseAddressIPv6('::')) + 0 + >>> print(_parseAddressIPv6('::1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0::0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:0')) + 0 + >>> print(_parseAddressIPv6('0:0:0::0:0:0')) + 0 - >>> _parseAddressIPv6('FEDC:BA98:7654:3210:FEDC:BA98:7654:3210') - 338770000845734292534325025077361652240L - >>> _parseAddressIPv6('1080:0000:0000:0000:0008:0800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('1080:0:0:0:8:800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('1080:0::8:800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('1080::8:800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('FF01:0:0:0:0:0:0:43') - 338958331222012082418099330867817087043L - >>> _parseAddressIPv6('FF01:0:0::0:0:43') - 338958331222012082418099330867817087043L - >>> _parseAddressIPv6('FF01::43') - 338958331222012082418099330867817087043L - >>> _parseAddressIPv6('0:0:0:0:0:0:13.1.68.3') - 218186755L - >>> _parseAddressIPv6('::13.1.68.3') - 218186755L - >>> _parseAddressIPv6('0:0:0:0:0:FFFF:129.144.52.38') - 281472855454758L - >>> _parseAddressIPv6('::FFFF:129.144.52.38') - 281472855454758L - >>> _parseAddressIPv6('1080:0:0:0:8:800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('1080::8:800:200C:417A') - 21932261930451111902915077091070067066L - >>> _parseAddressIPv6('::1:2:3:4:5:6') - 1208962713947218704138246L - >>> _parseAddressIPv6('1:2:3:4:5:6::') - 5192455318486707404433266432802816L + >>> print(_parseAddressIPv6('FEDC:BA98:7654:3210:FEDC:BA98:7654:3210')) + 338770000845734292534325025077361652240 + >>> print(_parseAddressIPv6('1080:0000:0000:0000:0008:0800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('FF01:0:0:0:0:0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01:0:0::0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01::43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('::13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('0:0:0:0:0:FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('::FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('::1:2:3:4:5:6')) + 1208962713947218704138246 + >>> print(_parseAddressIPv6('1:2:3:4:5:6::')) + 5192455318486707404433266432802816 """ # Split string into a list, example: - # '1080:200C::417A' => ['1080', '200C', '417A'] and fill_pos=2 + # '1080:200C::417A' => ['1080', '200C', '417A'] and fill_pos=2 # and fill_pos is the position of '::' in the list items = [] index = 0 @@ -987,7 +1293,7 @@ def _parseAddressIPv6(ipstr): if items and '.' in items[-1]: # IPv6 ending with IPv4 like '::ffff:192.168.0.1' - if not (fill_pos <= len(items) - 1): + if (fill_pos is not None) and not (fill_pos <= len(items) - 1): # Invalid IPv6: 'ffff:192.168.0.1::' raise ValueError("%r: Invalid IPv6 address: '::' after IPv4" % ipstr) value = parseAddress(items[-1])[0] @@ -1007,12 +1313,12 @@ def _parseAddressIPv6(ipstr): raise ValueError("%r: Invalid IPv6 address: should have 8 hextets" % ipstr) # Convert strings to long integer - value = 0L + value = 0 index = 0 for item in items: try: item = int(item, 16) - error = not (0 <= item <= 0xFFFF) + error = not (0 <= item <= 0xffff) except ValueError: error = True if error: @@ -1029,99 +1335,122 @@ def parseAddress(ipstr): Following address formats are recognized: - >>> parseAddress('0x0123456789abcdef') # IPv4 if <= 0xffffffff else IPv6 - (81985529216486895L, 6) - >>> parseAddress('123.123.123.123') # IPv4 - (2071690107L, 4) - >>> parseAddress('123.123') # 0-padded IPv4 - (2071658496L, 4) - >>> parseAddress('1080:0000:0000:0000:0008:0800:200C:417A') - (21932261930451111902915077091070067066L, 6) - >>> parseAddress('1080:0:0:0:8:800:200C:417A') - (21932261930451111902915077091070067066L, 6) - >>> parseAddress('1080:0::8:800:200C:417A') - (21932261930451111902915077091070067066L, 6) - >>> parseAddress('::1') - (1L, 6) - >>> parseAddress('::') - (0L, 6) - >>> parseAddress('0:0:0:0:0:FFFF:129.144.52.38') - (281472855454758L, 6) - >>> parseAddress('::13.1.68.3') - (218186755L, 6) - >>> parseAddress('::FFFF:129.144.52.38') - (281472855454758L, 6) + >>> def testParseAddress(address): + ... ip, version = parseAddress(address) + ... print(("%s (IPv%s)" % (ip, version))) + ... + >>> testParseAddress('0x0123456789abcdef') # IPv4 if <= 0xffffffff else IPv6 + 81985529216486895 (IPv6) + >>> testParseAddress('123.123.123.123') # IPv4 + 2071690107 (IPv4) + >>> testParseAddress('123.123') # 0-padded IPv4 + 2071658496 (IPv4) + >>> testParseAddress('127') + 2130706432 (IPv4) + >>> testParseAddress('255') + 4278190080 (IPv4) + >>> testParseAddress('256') + 256 (IPv4) + >>> testParseAddress('108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('0x108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0000:0000:0000:0008:0800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0:0:0:8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0::8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('::1') + 1 (IPv6) + >>> testParseAddress('::') + 0 (IPv6) + >>> testParseAddress('0:0:0:0:0:FFFF:129.144.52.38') + 281472855454758 (IPv6) + >>> testParseAddress('::13.1.68.3') + 218186755 (IPv6) + >>> testParseAddress('::FFFF:129.144.52.38') + 281472855454758 (IPv6) """ - if ipstr.startswith('0x'): - ret = long(ipstr[2:], 16) - if ret > 0xffffffffffffffffffffffffffffffffL: - raise ValueError, "%r: IP Address can't be bigger than 2^128" % (ipstr) - if ret < 0x100000000L: - return (ret, 4) + try: + hexval = int(ipstr, 16) + except ValueError: + hexval = None + try: + intval = int(ipstr, 10) + except ValueError: + intval = None + + if ipstr.startswith('0x') and hexval is not None: + if hexval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, hexval)) + if hexval <= MAX_IPV4_ADDRESS: + return (hexval, 4) else: - return (ret, 6) + return (hexval, 6) if ipstr.find(':') != -1: return (_parseAddressIPv6(ipstr), 6) - elif len(ipstr) == 32: + elif len(ipstr) == 32 and hexval is not None: # assume IPv6 in pure hexadecimal notation - return (long(ipstr, 16), 6) + return (hexval, 6) - elif ipstr.find('.') != -1 or (len(ipstr) < 4 and int(ipstr) < 256): + elif ipstr.find('.') != -1 or (intval is not None and intval < 256): # assume IPv4 ('127' gets interpreted as '127.0.0.0') bytes = ipstr.split('.') if len(bytes) > 4: - raise ValueError, "IPv4 Address with more than 4 bytes" + raise ValueError("IPv4 Address with more than 4 bytes") bytes += ['0'] * (4 - len(bytes)) - bytes = [long(x) for x in bytes] + bytes = [int(x) for x in bytes] for x in bytes: if x > 255 or x < 0: - raise ValueError, "%r: single byte must be 0 <= byte < 256" % (ipstr) + raise ValueError("%r: single byte must be 0 <= byte < 256" % (ipstr)) return ((bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3], 4) - else: + elif intval is not None: # we try to interprete it as a decimal digit - # this ony works for numbers > 255 ... others # will be interpreted as IPv4 first byte - ret = long(ipstr, 10) - if ret > 0xffffffffffffffffffffffffffffffffL: - raise ValueError, "IP Address can't be bigger than 2^128" - if ret <= 0xffffffffL: - return (ret, 4) + if intval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, intval)) + if intval <= MAX_IPV4_ADDRESS: + return (intval, 4) else: - return (ret, 6) + return (intval, 6) + + raise ValueError("IP Address format was invalid: %s" % ipstr) def intToIp(ip, version): """Transform an integer string into an IP address.""" - # just to be sure and hoping for Python 2.22 - ip = long(ip) + # just to be sure and hoping for Python 2.2 + ip = int(ip) if ip < 0: - raise ValueError, "IPs can't be negative: %d" % (ip) + raise ValueError("IPs can't be negative: %d" % (ip)) ret = '' if version == 4: - if ip > 0xffffffffL: - raise ValueError, "IPv4 Addresses can't be larger than 0xffffffff: %s" % (hex(ip)) - for l in range(4): - ret = str(ip & 0xffL) + '.' + ret + if ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, ip)) + for l in xrange(4): + ret = str(ip & 0xff) + '.' + ret ip = ip >> 8 ret = ret[:-1] elif version == 6: - if ip > 0xffffffffffffffffffffffffffffffffL: - raise ValueError, "IPv6 Addresses can't be larger than 0xffffffffffffffffffffffffffffffff: %s" % (hex(ip)) - l = '0' * 32 + hex(ip)[2:-1] - for x in range(1, 33): + if ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, ip)) + l = "%032x" % ip + for x in xrange(1, 33): ret = l[-x] + ret if x % 4 == 0: ret = ':' + ret ret = ret[1:] else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") return ret @@ -1137,7 +1466,7 @@ def _ipVersionToLen(version): Traceback (most recent call last): File "<stdin>", line 1, in ? File "IPy.py", line 1076, in _ipVersionToLen - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") ValueError: only IPv4 and IPv6 supported """ @@ -1146,7 +1475,7 @@ def _ipVersionToLen(version): elif version == 6: return 128 else: - raise ValueError, "only IPv4 and IPv6 supported" + raise ValueError("only IPv4 and IPv6 supported") def _countFollowingZeros(l): @@ -1169,15 +1498,10 @@ def _intToBin(val): """Return the binary representation of an integer as string.""" if val < 0: - raise ValueError, "Only positive values allowed" - s = hex(val).lower() + raise ValueError("Only positive values allowed") + s = "%x" % val ret = '' - if s[-1] == 'l': - s = s[:-1] - for x in s[2:]: - if __debug__: - if not _BitTable.has_key(x): - raise AssertionError, "hex() returned strange result" + for x in s: ret += _BitTable[x] # remove leading zeros while ret[0] == '0' and len(ret) > 1: @@ -1197,10 +1521,10 @@ def _count1Bits(num): def _count0Bits(num): """Find the highest bit set to 0 in an integer.""" - # this could be so easy if _count1Bits(~long(num)) would work as excepted - num = long(num) + # this could be so easy if _count1Bits(~int(num)) would work as excepted + num = int(num) if num < 0: - raise ValueError, "Only positive Numbers please: %s" % (num) + raise ValueError("Only positive Numbers please: %s" % (num)) ret = 0 while num > 0: if num & 1 == 1: @@ -1216,13 +1540,13 @@ def _checkPrefix(ip, prefixlen, version): Checks if the variant part of a prefix only has 0s, and the length is correct. - >>> _checkPrefix(0x7f000000L, 24, 4) + >>> _checkPrefix(0x7f000000, 24, 4) 1 - >>> _checkPrefix(0x7f000001L, 24, 4) + >>> _checkPrefix(0x7f000001, 24, 4) 0 - >>> repr(_checkPrefix(0x7f000001L, -1, 4)) + >>> repr(_checkPrefix(0x7f000001, -1, 4)) 'None' - >>> repr(_checkPrefix(0x7f000001L, 33, 4)) + >>> repr(_checkPrefix(0x7f000001, 33, 4)) 'None' """ @@ -1245,7 +1569,7 @@ def _checkPrefix(ip, prefixlen, version): def _checkNetmask(netmask, masklen): """Checks if a netmask is expressable as a prefixlen.""" - num = long(netmask) + num = int(netmask) bits = masklen # remove zero bits at the end @@ -1257,17 +1581,17 @@ def _checkNetmask(netmask, masklen): # now check if the rest consists only of ones while bits > 0: if (num & 1) == 0: - raise ValueError, "Netmask %s can't be expressed as an prefix." % (hex(netmask)) + raise ValueError("Netmask 0x%x can't be expressed as an prefix." % netmask) num = num >> 1 bits -= 1 def _checkNetaddrWorksWithPrefixlen(net, prefixlen, version): """Check if a base addess of a network is compatible with a prefixlen""" - if net & _prefixlenToNetmask(prefixlen, version) == net: - return 1 - else: - return 0 + try: + return (net & _prefixlenToNetmask(prefixlen, version) == net) + except ValueError: + return False def _netmaskToPrefixlen(netmask): @@ -1292,8 +1616,28 @@ def _prefixlenToNetmask(prefixlen, version): if prefixlen == 0: return 0 elif prefixlen < 0: - raise ValueError, "Prefixlen must be > 0" - return ((2L << prefixlen - 1) - 1) << (_ipVersionToLen(version) - prefixlen) + raise ValueError("Prefixlen must be > 0") + return ((2 << prefixlen - 1) - 1) << (_ipVersionToLen(version) - prefixlen) + + +def _remove_subprefix(prefix, subprefix): + if prefix in subprefix: + # Nothing left + return IPSet() + + if subprefix not in prefix: + # That prefix isn't even in here + return IPSet([IP(prefix)]) + + # Start cutting in half, recursively + prefixes = [ + IP('%s/%d' % (prefix[0], prefix._prefixlen + 1)), + IP('%s/%d' % (prefix[int(prefix.len() / 2)], prefix._prefixlen + 1)), + ] + if subprefix in prefixes[0]: + return _remove_subprefix(prefixes[0], subprefix) + IPSet([prefixes[1]]) + else: + return IPSet([prefixes[0]]) + _remove_subprefix(prefixes[1], subprefix) if __name__ == "__main__": diff --git a/vrtManager/create.py b/vrtManager/create.py index 5a41a6a..fd3203a 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -118,9 +118,13 @@ class wvmCreate(wvmConnect): vol = self.get_volume_by_path(vol_path) return vol.storagePoolLookupByVolume() - def clone_from_template(self, clone, template, metadata=False, owner=default_owner): + def clone_from_template(self, clone, template, storage=None, metadata=False, owner=default_owner): vol = self.get_volume_by_path(template) - stg = vol.storagePoolLookupByVolume() + if not storage: + stg = vol.storagePoolLookupByVolume() + else: + stg = self.get_storage(storage) + storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") format = util.get_xml_path(vol.XMLDesc(0), "/volume/target/format/@type") if storage_type == 'dir': diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 6ebb5c8..143465b 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -201,7 +201,6 @@ class wvmInstance(wvmConnect): return util.get_xml_path(self._XMLDesc(0), func=filterrefs) - def get_description(self): description = util.get_xml_path(self._XMLDesc(0), "/domain/description") return description if description else '' @@ -248,13 +247,8 @@ class wvmInstance(wvmConnect): def get_disk_devices(self): def disks(doc): result = [] - dev = None - volume = None - storage = None - src_fl = None - disk_format = None - used_size = None - disk_size = None + dev = volume = storage = src_file = None + disk_format = used_size = disk_size = disk_cache = None for disk in doc.xpath('/domain/devices/disk'): device = disk.xpath('@device')[0] @@ -262,13 +256,17 @@ class wvmInstance(wvmConnect): try: dev = disk.xpath('target/@dev')[0] bus = disk.xpath('target/@bus')[0] - src_fl = disk.xpath('source/@file|source/@dev|source/@name|source/@volume')[0] + src_file = disk.xpath('source/@file|source/@dev|source/@name|source/@volume')[0] try: disk_format = disk.xpath('driver/@type')[0] except: pass try: - vol = self.get_volume_by_path(src_fl) + disk_cache = disk.xpath('driver/@cache')[0] + except: + pass + try: + vol = self.get_volume_by_path(src_file) volume = vol.name() disk_size = vol.info()[1] @@ -276,13 +274,13 @@ class wvmInstance(wvmConnect): stg = vol.storagePoolLookupByVolume() storage = stg.name() except libvirtError: - volume = src_fl + volume = src_file except: pass finally: result.append( - {'dev': dev, 'bus': bus, 'image': volume, 'storage': storage, 'path': src_fl, - 'format': disk_format, 'size': disk_size, 'used': used_size}) + {'dev': dev, 'bus': bus, 'image': volume, 'storage': storage, 'path': src_file, + 'format': disk_format, 'size': disk_size, 'used': used_size, 'cache': disk_cache}) return result return util.get_xml_path(self._XMLDesc(0), func=disks) @@ -290,10 +288,8 @@ class wvmInstance(wvmConnect): def get_media_devices(self): def disks(doc): result = [] - dev = None - volume = None - storage = None - src_fl = None + dev = volume = storage = None + src_file = None for media in doc.xpath('/domain/devices/disk'): device = media.xpath('@device')[0] if device == 'cdrom': @@ -301,22 +297,137 @@ class wvmInstance(wvmConnect): dev = media.xpath('target/@dev')[0] bus = media.xpath('target/@bus')[0] try: - src_fl = media.xpath('source/@file')[0] - vol = self.get_volume_by_path(src_fl) + src_file = media.xpath('source/@file')[0] + vol = self.get_volume_by_path(src_file) volume = vol.name() stg = vol.storagePoolLookupByVolume() storage = stg.name() except: - src_fl = None - volume = src_fl + src_file = None + volume = src_file except: pass finally: - result.append({'dev': dev, 'image': volume, 'storage': storage, 'path': src_fl, 'bus': bus}) + result.append({'dev': dev, 'image': volume, 'storage': storage, 'path': src_file, 'bus': bus}) return result return util.get_xml_path(self._XMLDesc(0), func=disks) + def get_bootmenu(self): + menu = util.get_xml_path(self._XMLDesc(0), "/domain/os/bootmenu/@enable") + return True if menu == 'yes' else False + + def set_bootmenu(self, flag): + tree = ElementTree.fromstring(self._XMLDesc(0)) + os = tree.find('os') + menu = os.find("bootmenu") + + if menu == None: + bootmenu = ElementTree.fromstring("<bootmenu enable='yes'/>") + os.append(bootmenu) + menu = os.find("bootmenu") + + if flag == 0: # Disable + menu.attrib['enable'] = 'no' + elif flag == 1: # Enable + menu.attrib['enable'] = 'yes' + elif flag == -1: # Remove + os.remove(menu) + else: + raise Exception('Unknown boot menu option, please choose one of 0:disable, 1:enable, -1:remove') + + xmldom = ElementTree.tostring(tree) + self._defineXML(xmldom) + + def get_bootorder(self): + boot_order = {} + tree = ElementTree.fromstring(self._XMLDesc(0)) + os = tree.find('os') + boot = os.findall('boot') + + for idx, b in enumerate(boot): + dev = b.get('dev') + if dev == 'hd': + target = "disk" + type = "file" + elif dev == 'fd': + target = "floppy" + type = "file" + elif dev == 'cdrom': + target = "cdrom" + type = "file" + elif dev == 'network': + target = "network" + type = "network" + boot_order[idx] = {"type": type, "dev": dev, "target": target} + + devices = tree.find('devices') + for dev in devices: + dev_target = dev_type = dev_device = dev_alias = None + boot_dev = dev.find('boot') + if boot_dev != None: + idx = boot_dev.get('order') + dev_type = dev.get('type') + dev_device = dev.get('device') + + if dev_type == 'file': + dev_target = dev.find('target').get('dev') + + elif dev_type == 'network': + dev_mac = dev.find('mac').get('address') + dev_device = "network" + dev_target = "nic-{}".format(dev_mac[9:]) + elif dev_type == 'usb': + pass + + boot_order[int(idx)-1] = {"type": dev_type, "dev": dev_device, "target": dev_target} + + return boot_order + + def set_bootorder(self, devorder): + if not devorder: + return + + def remove_bootorder(): + tree = ElementTree.fromstring(self._XMLDesc(0)) + os = tree.find('os') + boot = os.findall('boot') + # Remove old style boot order + for b in boot: + os.remove(b) + # Remove rest of them + for dev in tree.find('devices'): + boot_dev = dev.find('boot') + if boot_dev != None: + dev.remove(boot_dev) + return tree + + tree = remove_bootorder() + + for idx, dev in devorder.items(): + order = ElementTree.fromstring("<boot order='{}'/>".format(idx + 1)) + if dev['type'] == 'disk': + devices = tree.findall("./devices/disk[@device='disk']") + for d in devices: + device = d.find("./target[@dev='{}']".format(dev['dev'])) + if device != None: + d.append(order) + elif dev['type'] == 'cdrom': + devices = tree.findall("./devices/disk[@device='cdrom']") + for d in devices: + device = d.find("./target[@dev='{}']".format(dev['dev'])) + if device != None: + d.append(order) + elif dev['type'] == 'network': + devices = tree.findall("./devices/interface[@type='network']") + for d in devices: + device = d.find("mac[@address='{}']".format(dev['dev'])) + if device != None: + d.append(order) + else: + raise Exception('Invalid Device Type for boot order') + self._defineXML(ElementTree.tostring(tree)) + def mount_iso(self, dev, image): def attach_iso(dev, disk, vol): if disk.get('device') == 'cdrom': @@ -490,11 +601,9 @@ class wvmInstance(wvmConnect): return telnet_port def get_console_listen_addr(self): - listen_addr = util.get_xml_path(self._XMLDesc(0), - "/domain/devices/graphics/@listen") + listen_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@listen") if listen_addr is None: - listen_addr = util.get_xml_path(self._XMLDesc(0), - "/domain/devices/graphics/listen/@address") + listen_addr = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/listen/@address") if listen_addr is None: return "127.0.0.1" return listen_addr diff --git a/vrtManager/network.py b/vrtManager/network.py index c1de4f3..32b0819 100644 --- a/vrtManager/network.py +++ b/vrtManager/network.py @@ -1,6 +1,10 @@ from vrtManager import util from vrtManager.IPy import IP from vrtManager.connection import wvmConnect +from xml.etree import ElementTree +from libvirt import VIR_NETWORK_SECTION_IP_DHCP_HOST, VIR_NETWORK_SECTION_IP_DHCP_RANGE +from libvirt import VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, VIR_NETWORK_UPDATE_COMMAND_DELETE, VIR_NETWORK_UPDATE_COMMAND_MODIFY +from libvirt import VIR_NETWORK_UPDATE_AFFECT_LIVE, VIR_NETWORK_UPDATE_AFFECT_CONFIG def network_size(net, dhcp=None): @@ -106,6 +110,9 @@ class wvmNetwork(wvmConnect): def delete(self): self.net.undefine() + def update(self, command, section, parentIndex, xml, flags=0): + return self.net.update(command, section, parentIndex, xml, flags) + def get_ipv4_network(self): xml = self._XMLDesc(0) if util.get_xml_path(xml, "/network/ip") is None: @@ -169,9 +176,57 @@ class wvmNetwork(wvmConnect): def network(doc): result = [] for net in doc.xpath('/network/ip/dhcp/host'): - host = net.xpath('@ip')[0] + ip = net.xpath('@ip')[0] mac = net.xpath('@mac')[0] - result.append({'host': host, 'mac': mac}) + name = net.xpath('@name')[0] + result.append({'ip': ip, 'mac': mac, 'name': name}) return result return util.get_xml_path(self._XMLDesc(0), func=network) + + def modify_fixed_address(self, name, address, mac): + util.validate_macaddr(mac) + new_xml = '<host mac="{}" name="{}" ip="{}"/>'.format(mac, name, IP(address)) + new_host_xml = ElementTree.fromstring(new_xml) + + tree = ElementTree.fromstring(self._XMLDesc(0)) + hosts = tree.findall("./ip/dhcp/host") + + host = None + for h in hosts: + if h.get('mac') == mac: + host = h + break + if host is None: + self.update(VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, VIR_NETWORK_SECTION_IP_DHCP_HOST, -1, new_xml, + VIR_NETWORK_UPDATE_AFFECT_LIVE|VIR_NETWORK_UPDATE_AFFECT_CONFIG) + else: + # change the host + if host.get('name') == new_host_xml.get('name') and host.get('ip') == new_host_xml.get('ip'): + return False + else: + self.update(VIR_NETWORK_UPDATE_COMMAND_MODIFY, VIR_NETWORK_SECTION_IP_DHCP_HOST, -1, new_xml, + VIR_NETWORK_UPDATE_AFFECT_LIVE|VIR_NETWORK_UPDATE_AFFECT_CONFIG) + + def delete_fixed_address(self, mac): + util.validate_macaddr(mac) + tree = ElementTree.fromstring(self._XMLDesc(0)) + hosts = tree.findall("./ip/dhcp/host") + + for h in hosts: + if h.get('mac') == mac: + new_xml = '<host mac="{}" name="{}" ip="{}"/>'.format(mac, h.get('name'), h.get('ip')) + self.update(VIR_NETWORK_UPDATE_COMMAND_DELETE, VIR_NETWORK_SECTION_IP_DHCP_HOST, -1, new_xml, + VIR_NETWORK_UPDATE_AFFECT_LIVE|VIR_NETWORK_UPDATE_AFFECT_CONFIG) + break + + def modify_dhcp_range(self, range_start, range_end): + if not self.is_active(): + new_range = '<range start="{}" end="{}"/>'.format(range_start, range_end) + tree = ElementTree.fromstring(self._XMLDesc(0)) + dhcp = tree.find("./ip/dhcp") + old_range = dhcp.find('range') + dhcp.remove(old_range) + dhcp.append(ElementTree.fromstring(new_range)) + + self.wvm.networkDefineXML(ElementTree.tostring(tree)) diff --git a/vrtManager/util.py b/vrtManager/util.py index 840e6a5..b4aa949 100644 --- a/vrtManager/util.py +++ b/vrtManager/util.py @@ -153,3 +153,15 @@ def validate_uuid(val): val = (val[0:8] + "-" + val[8:12] + "-" + val[12:16] + "-" + val[16:20] + "-" + val[20:32]) return val + + +def validate_macaddr(val): + if val is None: + return + + if not (isinstance(val, str) or isinstance(val, basestring)): + raise ValueError("MAC address must be a string.") + + form = re.match("^([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}$", val) + if form is None: + raise ValueError("MAC address must be of the format AA:BB:CC:DD:EE:FF, was '%s'" % val)