wip
This commit is contained in:
parent
45c1bdf440
commit
b1a625a0e4
45 changed files with 1911 additions and 0 deletions
1
interactive_discuss_ai/__init__.py
Normal file
1
interactive_discuss_ai/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
46
interactive_discuss_ai/__manifest__.py
Normal file
46
interactive_discuss_ai/__manifest__.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
'name': 'Interactive AI Voice Assistant',
|
||||
'version': '17.0.1.0.0',
|
||||
'category': 'Productivity/Communication',
|
||||
'summary': 'Natural Language Voice Interaction for Odoo using Open WebUI',
|
||||
'sequence': 1,
|
||||
'author': 'Benoît Vézina',
|
||||
'website': 'https://bemade.org',
|
||||
'maintainer': 'it@bemade.org',
|
||||
'license': 'LGPL-3',
|
||||
'description': """
|
||||
Interactive AI Voice Assistant for Odoo
|
||||
=====================================
|
||||
|
||||
This module enables natural language voice interaction with Odoo directly in the Discuss interface:
|
||||
- Voice input processing using Open WebUI models
|
||||
- Interactive AI chat assistant
|
||||
- Smart context-aware responses
|
||||
- Draft mode for new records creation
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
'web',
|
||||
'mail',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/ai_assistant_views.xml',
|
||||
'views/ai_config_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'interactive_discuss_ai/static/src/js/ai_assistant_chat.js',
|
||||
'interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml',
|
||||
],
|
||||
},
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'external_dependencies': {
|
||||
'python': [
|
||||
'requests',
|
||||
],
|
||||
},
|
||||
}
|
||||
2
interactive_discuss_ai/models/__init__.py
Normal file
2
interactive_discuss_ai/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import ai_assistant
|
||||
from . import ai_config
|
||||
101
interactive_discuss_ai/models/ai_assistant.py
Normal file
101
interactive_discuss_ai/models/ai_assistant.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class AIAssistantChannel(models.Model):
|
||||
_name = 'ai.assistant.channel'
|
||||
_description = 'AI Assistant Channel'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(default="Assistant AI", readonly=True, tracking=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
tracking=True
|
||||
)
|
||||
active = fields.Boolean(default=True, tracking=True)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='User',
|
||||
required=True,
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True
|
||||
)
|
||||
message_ids = fields.One2many(
|
||||
'mail.message',
|
||||
'res_id',
|
||||
string='Messages',
|
||||
domain=lambda self: [('model', '=', self._name)],
|
||||
tracking=True
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
# Create a dedicated discussion channel for the assistant
|
||||
channel = self.env['mail.channel'].create({
|
||||
'name': _('Assistant AI - %s', record.company_id.name),
|
||||
'channel_type': 'chat',
|
||||
'channel_partner_ids': [(4, record.user_id.partner_id.id)],
|
||||
})
|
||||
return records
|
||||
|
||||
def _get_conversation_history(self, limit=10):
|
||||
"""Get recent conversation history"""
|
||||
self.ensure_one()
|
||||
messages = self.env['mail.message'].search([
|
||||
('model', '=', self._name),
|
||||
('res_id', '=', self.id)
|
||||
], limit=limit, order='id desc')
|
||||
|
||||
history = []
|
||||
for msg in reversed(messages):
|
||||
role = "assistant" if msg.author_id == self.user_id.partner_id else "user"
|
||||
history.append({
|
||||
"role": role,
|
||||
"content": msg.body
|
||||
})
|
||||
return history
|
||||
|
||||
def process_voice_message(self, voice_input):
|
||||
"""Process voice input and respond"""
|
||||
self.ensure_one()
|
||||
if not voice_input:
|
||||
raise UserError(_('No voice input provided'))
|
||||
|
||||
# Get conversation history
|
||||
conversation_history = self._get_conversation_history()
|
||||
|
||||
# Use AI service to generate response
|
||||
ai_service = self.env['ai.service'].with_company(self.company_id)
|
||||
response = ai_service.generate_response(voice_input, conversation_history)
|
||||
|
||||
# Post response as message
|
||||
self.message_post(
|
||||
body=response,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment'
|
||||
)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_assistant_for_user(self, user_id=None):
|
||||
"""Get or create an assistant for the user in their company"""
|
||||
if not user_id:
|
||||
user_id = self.env.user.id
|
||||
|
||||
assistant = self.search([
|
||||
('user_id', '=', user_id),
|
||||
('company_id', '=', self.env.company.id),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not assistant:
|
||||
assistant = self.create({
|
||||
'user_id': user_id,
|
||||
'company_id': self.env.company.id
|
||||
})
|
||||
|
||||
return assistant
|
||||
61
interactive_discuss_ai/models/ai_config.py
Normal file
61
interactive_discuss_ai/models/ai_config.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class AIConfig(models.Model):
|
||||
_name = 'ai.config'
|
||||
_description = 'AI Configuration'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(string='Name', required=True, default='Default Config', tracking=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
tracking=True
|
||||
)
|
||||
openwebui_url = fields.Char(
|
||||
string='Open WebUI URL',
|
||||
required=True,
|
||||
default='http://localhost:8080',
|
||||
tracking=True
|
||||
)
|
||||
model_name = fields.Char(
|
||||
string='Model Name',
|
||||
required=True,
|
||||
help='Nom du modèle à utiliser dans Open WebUI',
|
||||
tracking=True
|
||||
)
|
||||
system_prompt = fields.Text(
|
||||
string='System Prompt',
|
||||
default="""Tu es un assistant Odoo expert qui aide les utilisateurs à effectuer des actions
|
||||
dans le système. Analyse leurs demandes et propose des actions concrètes.""",
|
||||
tracking=True
|
||||
)
|
||||
temperature = fields.Float(
|
||||
string='Temperature',
|
||||
default=0.7,
|
||||
help='Contrôle la créativité des réponses (0.0 - 1.0)',
|
||||
tracking=True
|
||||
)
|
||||
max_tokens = fields.Integer(
|
||||
string='Max Tokens',
|
||||
default=2000,
|
||||
tracking=True
|
||||
)
|
||||
active = fields.Boolean(default=True, tracking=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('company_uniq', 'unique(company_id, active)',
|
||||
'Une seule configuration active par compagnie est autorisée!')
|
||||
]
|
||||
|
||||
@api.model
|
||||
def get_default_config(self):
|
||||
"""Récupère la configuration active pour la compagnie actuelle"""
|
||||
config = self.search([
|
||||
('active', '=', True),
|
||||
('company_id', '=', self.env.company.id)
|
||||
], limit=1)
|
||||
if not config:
|
||||
raise UserError('Aucune configuration AI active trouvée pour votre compagnie')
|
||||
62
interactive_discuss_ai/models/ai_service.py
Normal file
62
interactive_discuss_ai/models/ai_service.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import requests
|
||||
import json
|
||||
from odoo import models, tools
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class AIService(models.AbstractModel):
|
||||
_name = 'ai.service'
|
||||
_description = 'AI Service for Open WebUI Integration'
|
||||
|
||||
def _get_headers(self):
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
def _call_openwebui_api(self, endpoint, payload):
|
||||
"""Appelle l'API Open WebUI avec la configuration de la compagnie actuelle"""
|
||||
config = self.env['ai.config'].with_company(self.env.company).get_default_config()
|
||||
url = f"{config.openwebui_url}/api/v1/{endpoint}"
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise UserError(f"Erreur de communication avec Open WebUI: {str(e)}")
|
||||
|
||||
def generate_response(self, user_input, conversation_history=None):
|
||||
"""Génère une réponse en utilisant le modèle Open WebUI de la compagnie"""
|
||||
config = self.env['ai.config'].with_company(self.env.company).get_default_config()
|
||||
|
||||
messages = []
|
||||
if config.system_prompt:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": config.system_prompt
|
||||
})
|
||||
|
||||
# Ajouter l'historique de conversation si disponible
|
||||
if conversation_history:
|
||||
messages.extend(conversation_history)
|
||||
|
||||
# Ajouter l'entrée utilisateur actuelle
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": user_input
|
||||
})
|
||||
|
||||
payload = {
|
||||
"model": config.model_name,
|
||||
"messages": messages,
|
||||
"temperature": config.temperature,
|
||||
"max_tokens": config.max_tokens,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
response = self._call_openwebui_api('chat/completions', payload)
|
||||
return response.get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
3
interactive_discuss_ai/security/ir.model.access.csv
Normal file
3
interactive_discuss_ai/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_ai_assistant_channel_user,ai.assistant.channel.user,interactive_discuss_ai.model_ai_assistant_channel,base.group_user,1,1,1,1
|
||||
access_ai_config_user,ai.config.user,interactive_discuss_ai.model_ai_config,base.group_user,1,1,1,1
|
||||
|
52
interactive_discuss_ai/static/src/js/ai_assistant_chat.js
Normal file
52
interactive_discuss_ai/static/src/js/ai_assistant_chat.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
import { attr } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerPatch({
|
||||
name: 'mail.composer',
|
||||
fields: {
|
||||
isVoiceRecording: attr({
|
||||
default: false,
|
||||
}),
|
||||
},
|
||||
recordMethods: {
|
||||
async onClickVoiceRecord() {
|
||||
if (!this.isVoiceRecording) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
this.mediaRecorder = new MediaRecorder(stream);
|
||||
const audioChunks = [];
|
||||
|
||||
this.mediaRecorder.addEventListener('dataavailable', (event) => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
this.mediaRecorder.addEventListener('stop', async () => {
|
||||
const audioBlob = new Blob(audioChunks);
|
||||
// Convert to base64 and send to backend
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(audioBlob);
|
||||
reader.onloadend = async () => {
|
||||
const base64data = reader.result;
|
||||
await this.env.services.rpc({
|
||||
model: 'ai.assistant.channel',
|
||||
method: 'process_voice_message',
|
||||
args: [this.thread.id, base64data],
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
this.mediaRecorder.start();
|
||||
this.update({ isVoiceRecording: true });
|
||||
} catch (err) {
|
||||
console.error('Error accessing microphone:', err);
|
||||
}
|
||||
} else {
|
||||
this.mediaRecorder.stop();
|
||||
this.update({ isVoiceRecording: false });
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
12
interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml
Normal file
12
interactive_discuss_ai/static/src/xml/ai_assistant_chat.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.Composer" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o-mail-Composer-buttons')]" position="inside">
|
||||
<button class="btn btn-light"
|
||||
t-on-click="onClickVoiceRecord"
|
||||
t-att-class="{ 'btn-danger': isVoiceRecording }">
|
||||
<i class="fa fa-microphone" t-att-class="{ 'fa-pulse': isVoiceRecording }"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
86
interactive_discuss_ai/views/ai_assistant_views.xml
Normal file
86
interactive_discuss_ai/views/ai_assistant_views.xml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- AI Assistant Channel Form View -->
|
||||
<record id="view_ai_assistant_channel_form" model="ir.ui.view">
|
||||
<field name="name">ai.assistant.channel.form</field>
|
||||
<field name="model">ai.assistant.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="AI Assistant Channel">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button" options="{"terminology": "archive"}"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Assistant Channel Tree View -->
|
||||
<record id="view_ai_assistant_channel_tree" model="ir.ui.view">
|
||||
<field name="name">ai.assistant.channel.tree</field>
|
||||
<field name="model">ai.assistant.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="AI Assistant Channels">
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Assistant Channel Search View -->
|
||||
<record id="view_ai_assistant_channel_search" model="ir.ui.view">
|
||||
<field name="name">ai.assistant.channel.search</field>
|
||||
<field name="model">ai.assistant.channel</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search AI Assistant Channels">
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Assistant Channel Action -->
|
||||
<record id="action_ai_assistant_channel" model="ir.actions.act_window">
|
||||
<field name="name">AI Assistant Channels</field>
|
||||
<field name="res_model">ai.assistant.channel</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_ai_assistant_channel_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first AI Assistant Channel!
|
||||
</p>
|
||||
<p>
|
||||
AI Assistant Channels allow you to interact with the AI assistant through messages.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem id="menu_ai_assistant"
|
||||
name="AI Assistant"
|
||||
web_icon="interactive_discuss_ai,static/description/icon.png"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_ai_assistant_channel"
|
||||
name="Assistant Channels"
|
||||
parent="menu_ai_assistant"
|
||||
action="action_ai_assistant_channel"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
83
interactive_discuss_ai/views/ai_config_views.xml
Normal file
83
interactive_discuss_ai/views/ai_config_views.xml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_ai_config_form" model="ir.ui.view">
|
||||
<field name="name">ai.config.form</field>
|
||||
<field name="model">ai.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="openwebui_url"/>
|
||||
<field name="model_name"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="temperature"/>
|
||||
<field name="max_tokens"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="System Prompt">
|
||||
<field name="system_prompt" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_ai_config_tree" model="ir.ui.view">
|
||||
<field name="name">ai.config.tree</field>
|
||||
<field name="model">ai.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="model_name"/>
|
||||
<field name="openwebui_url"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_ai_config_search" model="ir.ui.view">
|
||||
<field name="name">ai.config.search</field>
|
||||
<field name="model">ai.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="model_name"/>
|
||||
<separator/>
|
||||
<filter string="Archivé" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Compagnie" name="company" domain="[]" context="{'group_by': 'company_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_ai_config" model="ir.actions.act_window">
|
||||
<field name="name">Configuration AI</field>
|
||||
<field name="res_model">ai.config</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_ai_config_search"/>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_ai_config"
|
||||
name="Configuration AI"
|
||||
action="action_ai_config"
|
||||
parent="base.menu_administration"
|
||||
sequence="100"/>
|
||||
</odoo>
|
||||
90
odoo_proxmox_manager/README.md
Normal file
90
odoo_proxmox_manager/README.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Odoo Proxmox Manager
|
||||
|
||||
This module allows you to manage your Proxmox servers and virtual machines directly from Odoo 17.0.
|
||||
|
||||
## Features
|
||||
|
||||
- Manage multiple Proxmox servers and clusters
|
||||
- Monitor server status and resources
|
||||
- View and manage virtual machines
|
||||
- Start, stop, restart, suspend, and resume VMs
|
||||
- Group servers into clusters for better organization
|
||||
- Multi-company support
|
||||
- Role-based access control
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Odoo 17.0
|
||||
2. Python package requirements:
|
||||
```
|
||||
proxmoxer
|
||||
requests
|
||||
```
|
||||
|
||||
### Steps
|
||||
|
||||
1. Clone this repository into your Odoo addons directory
|
||||
2. Install the required Python packages:
|
||||
```bash
|
||||
pip install proxmoxer requests
|
||||
```
|
||||
3. Update your Odoo addons list
|
||||
4. Install the module through Odoo's Apps menu
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create an API Token in your Proxmox server:
|
||||
- Log in to your Proxmox web interface
|
||||
- Go to Datacenter -> Permissions -> API Tokens
|
||||
- Create a new token and note down the Token ID and Secret
|
||||
|
||||
2. In Odoo:
|
||||
- Go to the Proxmox menu
|
||||
- Create a new cluster (optional)
|
||||
- Create a new server:
|
||||
- Enter the server hostname
|
||||
- Enter your API token information
|
||||
- Test the connection
|
||||
|
||||
## Usage
|
||||
|
||||
### Managing Servers
|
||||
|
||||
1. Navigate to Proxmox -> Servers
|
||||
2. Create or select a server
|
||||
3. Click "Sync VMs" to synchronize virtual machines
|
||||
4. View server status and resource information
|
||||
|
||||
### Managing Virtual Machines
|
||||
|
||||
1. Navigate to Proxmox -> Virtual Machines
|
||||
2. View all VMs across your servers
|
||||
3. Use the action buttons to:
|
||||
- Start VMs
|
||||
- Stop VMs
|
||||
- Restart VMs
|
||||
- Suspend VMs
|
||||
- Resume VMs
|
||||
|
||||
### Managing Clusters
|
||||
|
||||
1. Navigate to Proxmox -> Clusters
|
||||
2. Create a new cluster
|
||||
3. Add servers to the cluster
|
||||
4. Use "Sync All Servers" to update all servers in the cluster
|
||||
|
||||
## Security
|
||||
|
||||
The module includes two user groups:
|
||||
- Proxmox User: Can view servers and VMs
|
||||
- Proxmox Manager: Can manage servers and VMs
|
||||
|
||||
## Support
|
||||
|
||||
For bugs or feature requests, please create an issue in the repository.
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3
|
||||
3
odoo_proxmox_manager/__init__.py
Normal file
3
odoo_proxmox_manager/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
from . import wizard
|
||||
35
odoo_proxmox_manager/__manifest__.py
Normal file
35
odoo_proxmox_manager/__manifest__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
'name': 'Proxmox Manager',
|
||||
'version': '17.0.1.0.0',
|
||||
'category': 'Administration',
|
||||
'summary': 'Manage Proxmox servers and clusters from Odoo',
|
||||
'sequence': 1,
|
||||
'author': 'DurPro',
|
||||
'website': 'https://www.durpro.com',
|
||||
'license': 'LGPL-3',
|
||||
'icon': '/odoo_proxmox_manager/static/description/icon.png',
|
||||
'depends': [
|
||||
'base',
|
||||
'web',
|
||||
'mail'
|
||||
],
|
||||
'data': [
|
||||
'security/proxmox_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/proxmox_server_views.xml',
|
||||
'views/proxmox_cluster_views.xml',
|
||||
'views/proxmox_vm_views.xml',
|
||||
'views/proxmox_menus.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'odoo_proxmox_manager/static/src/components/**/*',
|
||||
'odoo_proxmox_manager/static/src/js/dashboard_view.js',
|
||||
'odoo_proxmox_manager/static/src/scss/dashboard.scss',
|
||||
'odoo_proxmox_manager/static/src/xml/dashboard.xml',
|
||||
],
|
||||
},
|
||||
'application': True,
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
1
odoo_proxmox_manager/controllers/__init__.py
Normal file
1
odoo_proxmox_manager/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
9
odoo_proxmox_manager/controllers/main.py
Normal file
9
odoo_proxmox_manager/controllers/main.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class ProxmoxController(http.Controller):
|
||||
|
||||
@http.route('/proxmox/dashboard/data', type='json', auth='user')
|
||||
def get_dashboard_data(self):
|
||||
"""Get dashboard data for the Proxmox overview"""
|
||||
return request.env['proxmox.server'].get_dashboard_data()
|
||||
3
odoo_proxmox_manager/models/__init__.py
Normal file
3
odoo_proxmox_manager/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import proxmox_server
|
||||
from . import proxmox_cluster
|
||||
from . import proxmox_vm
|
||||
37
odoo_proxmox_manager/models/proxmox_cluster.py
Normal file
37
odoo_proxmox_manager/models/proxmox_cluster.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
class ProxmoxCluster(models.Model):
|
||||
_name = 'proxmox.cluster'
|
||||
_description = 'Proxmox Cluster'
|
||||
_order = 'name'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(string='Name', required=True, tracking=True)
|
||||
description = fields.Text(string='Description', tracking=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||
server_ids = fields.One2many('proxmox.server', 'cluster_id', string='Servers')
|
||||
server_count = fields.Integer(string='Server Count', compute='_compute_server_count')
|
||||
total_vms = fields.Integer(string='Total VMs', compute='_compute_total_vms')
|
||||
active_servers = fields.Integer(string='Active Servers', compute='_compute_active_servers')
|
||||
|
||||
@api.depends('server_ids')
|
||||
def _compute_server_count(self):
|
||||
for cluster in self:
|
||||
cluster.server_count = len(cluster.server_ids)
|
||||
|
||||
@api.depends('server_ids.vm_ids')
|
||||
def _compute_total_vms(self):
|
||||
for cluster in self:
|
||||
cluster.total_vms = sum(server.vm_count for server in cluster.server_ids)
|
||||
|
||||
@api.depends('server_ids.state')
|
||||
def _compute_active_servers(self):
|
||||
for cluster in self:
|
||||
cluster.active_servers = len(cluster.server_ids.filtered(lambda s: s.state == 'online'))
|
||||
|
||||
def action_sync_all_servers(self):
|
||||
"""Synchronize all servers in the cluster"""
|
||||
self.ensure_one()
|
||||
for server in self.server_ids:
|
||||
server.action_sync_vms()
|
||||
return True
|
||||
164
odoo_proxmox_manager/models/proxmox_server.py
Normal file
164
odoo_proxmox_manager/models/proxmox_server.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
from odoo import models, fields, api
|
||||
from proxmoxer import ProxmoxAPI
|
||||
import requests
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ProxmoxServer(models.Model):
|
||||
_name = 'proxmox.server'
|
||||
_description = 'Proxmox Server'
|
||||
_order = 'name'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(string='Name', required=True, tracking=True)
|
||||
hostname = fields.Char(string='Hostname', required=True, tracking=True)
|
||||
port = fields.Integer(string='Port', default=8006, tracking=True)
|
||||
cluster_id = fields.Many2one('proxmox.cluster', string='Cluster', tracking=True)
|
||||
username = fields.Char(string='Username', required=True, tracking=True)
|
||||
token_name = fields.Char(string='API Token Name', tracking=True)
|
||||
token_value = fields.Char(string='API Token Value', tracking=True)
|
||||
verify_ssl = fields.Boolean(string='Verify SSL', default=True, tracking=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||
state = fields.Selection([
|
||||
('offline', 'Offline'),
|
||||
('online', 'Online')
|
||||
], string='Status', default='offline', compute='_compute_state', store=True, tracking=True)
|
||||
vm_ids = fields.One2many('proxmox.vm', 'server_id', string='Virtual Machines')
|
||||
vm_count = fields.Integer(string='VM Count', compute='_compute_vm_count')
|
||||
node_info = fields.Text(string='Node Information', compute='_compute_node_info')
|
||||
|
||||
def _get_proxmox_connection(self):
|
||||
try:
|
||||
if not self.token_name or not self.token_value:
|
||||
raise ValueError("API Token is required")
|
||||
|
||||
return ProxmoxAPI(
|
||||
self.hostname,
|
||||
port=self.port,
|
||||
user=self.username,
|
||||
token_name=self.token_name,
|
||||
token_value=self.token_value,
|
||||
verify_ssl=self.verify_ssl
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to connect to Proxmox server {self.name}: {str(e)}")
|
||||
return None
|
||||
|
||||
@api.depends('hostname', 'port', 'token_name', 'token_value')
|
||||
def _compute_state(self):
|
||||
for server in self:
|
||||
try:
|
||||
proxmox = server._get_proxmox_connection()
|
||||
if proxmox:
|
||||
# Test connection by getting version info
|
||||
proxmox.version.get()
|
||||
server.state = 'online'
|
||||
else:
|
||||
server.state = 'offline'
|
||||
except:
|
||||
server.state = 'offline'
|
||||
|
||||
@api.depends('vm_ids')
|
||||
def _compute_vm_count(self):
|
||||
for server in self:
|
||||
server.vm_count = len(server.vm_ids)
|
||||
|
||||
def _compute_node_info(self):
|
||||
for server in self:
|
||||
try:
|
||||
proxmox = server._get_proxmox_connection()
|
||||
if proxmox:
|
||||
node_info = proxmox.nodes(server.hostname).status.get()
|
||||
server.node_info = str(node_info)
|
||||
else:
|
||||
server.node_info = "Unable to connect to server"
|
||||
except Exception as e:
|
||||
server.node_info = f"Error getting node info: {str(e)}"
|
||||
|
||||
def action_sync_vms(self):
|
||||
"""Synchronize VMs from Proxmox server"""
|
||||
self.ensure_one()
|
||||
proxmox = self._get_proxmox_connection()
|
||||
if not proxmox:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Get all VMs from the server
|
||||
vms = proxmox.nodes(self.hostname).qemu.get()
|
||||
|
||||
# Update or create VM records
|
||||
for vm in vms:
|
||||
vm_vals = {
|
||||
'name': vm.get('name'),
|
||||
'vmid': vm.get('vmid'),
|
||||
'status': vm.get('status'),
|
||||
'server_id': self.id,
|
||||
}
|
||||
existing_vm = self.env['proxmox.vm'].search([
|
||||
('server_id', '=', self.id),
|
||||
('vmid', '=', vm.get('vmid'))
|
||||
])
|
||||
if existing_vm:
|
||||
existing_vm.write(vm_vals)
|
||||
else:
|
||||
self.env['proxmox.vm'].create(vm_vals)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to sync VMs for server {self.name}: {str(e)}")
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def get_dashboard_data(self):
|
||||
"""Get dashboard data for the Proxmox overview"""
|
||||
servers = self.search([])
|
||||
clusters = self.env['proxmox.cluster'].search([])
|
||||
vms = self.env['proxmox.vm'].search([])
|
||||
|
||||
server_data = []
|
||||
for server in servers:
|
||||
try:
|
||||
proxmox = server._get_proxmox_connection()
|
||||
if proxmox:
|
||||
node_info = proxmox.nodes(server.hostname).status.get()
|
||||
memory_total = node_info.get('memory', {}).get('total', 0)
|
||||
memory_used = node_info.get('memory', {}).get('used', 0)
|
||||
memory_usage = round((memory_used / memory_total) * 100, 2) if memory_total else 0
|
||||
|
||||
cpu_usage = round(node_info.get('cpu', 0) * 100, 2)
|
||||
|
||||
server_data.append({
|
||||
'id': server.id,
|
||||
'name': server.name,
|
||||
'state': server.state,
|
||||
'vm_count': server.vm_count,
|
||||
'memory_usage': memory_usage,
|
||||
'cpu_usage': cpu_usage,
|
||||
})
|
||||
else:
|
||||
server_data.append({
|
||||
'id': server.id,
|
||||
'name': server.name,
|
||||
'state': 'offline',
|
||||
'vm_count': server.vm_count,
|
||||
'memory_usage': 0,
|
||||
'cpu_usage': 0,
|
||||
})
|
||||
except Exception as e:
|
||||
_logger.error(f"Error getting data for server {server.name}: {str(e)}")
|
||||
server_data.append({
|
||||
'id': server.id,
|
||||
'name': server.name,
|
||||
'state': 'offline',
|
||||
'vm_count': server.vm_count,
|
||||
'memory_usage': 0,
|
||||
'cpu_usage': 0,
|
||||
})
|
||||
|
||||
return {
|
||||
'server_count': len(servers),
|
||||
'cluster_count': len(clusters),
|
||||
'vm_count': len(vms),
|
||||
'servers': server_data,
|
||||
}
|
||||
87
odoo_proxmox_manager/models/proxmox_vm.py
Normal file
87
odoo_proxmox_manager/models/proxmox_vm.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
class ProxmoxVM(models.Model):
|
||||
_name = 'proxmox.vm'
|
||||
_description = 'Proxmox Virtual Machine'
|
||||
_order = 'name'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char(string='Name', required=True, tracking=True)
|
||||
vmid = fields.Integer(string='VM ID', required=True, tracking=True)
|
||||
server_id = fields.Many2one('proxmox.server', string='Server', required=True, tracking=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True,
|
||||
related='server_id.company_id', store=True, readonly=True)
|
||||
status = fields.Selection([
|
||||
('running', 'Running'),
|
||||
('stopped', 'Stopped'),
|
||||
('suspended', 'Suspended')
|
||||
], string='Status', default='stopped', tracking=True)
|
||||
memory = fields.Integer(string='Memory (MB)')
|
||||
cpus = fields.Integer(string='vCPUs')
|
||||
disk_size = fields.Float(string='Disk Size (GB)')
|
||||
ip_address = fields.Char(string='IP Address')
|
||||
|
||||
def action_start(self):
|
||||
"""Start the virtual machine"""
|
||||
self.ensure_one()
|
||||
proxmox = self.server_id._get_proxmox_connection()
|
||||
if proxmox:
|
||||
try:
|
||||
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.start.post()
|
||||
self.status = 'running'
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
return False
|
||||
|
||||
def action_stop(self):
|
||||
"""Stop the virtual machine"""
|
||||
self.ensure_one()
|
||||
proxmox = self.server_id._get_proxmox_connection()
|
||||
if proxmox:
|
||||
try:
|
||||
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.stop.post()
|
||||
self.status = 'stopped'
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
return False
|
||||
|
||||
def action_restart(self):
|
||||
"""Restart the virtual machine"""
|
||||
self.ensure_one()
|
||||
proxmox = self.server_id._get_proxmox_connection()
|
||||
if proxmox:
|
||||
try:
|
||||
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.reset.post()
|
||||
self.status = 'running'
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
return False
|
||||
|
||||
def action_suspend(self):
|
||||
"""Suspend the virtual machine"""
|
||||
self.ensure_one()
|
||||
proxmox = self.server_id._get_proxmox_connection()
|
||||
if proxmox:
|
||||
try:
|
||||
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.suspend.post()
|
||||
self.status = 'suspended'
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
return False
|
||||
|
||||
def action_resume(self):
|
||||
"""Resume the virtual machine"""
|
||||
self.ensure_one()
|
||||
proxmox = self.server_id._get_proxmox_connection()
|
||||
if proxmox:
|
||||
try:
|
||||
proxmox.nodes(self.server_id.hostname).qemu(self.vmid).status.resume.post()
|
||||
self.status = 'running'
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
return False
|
||||
2
odoo_proxmox_manager/requirements.txt
Normal file
2
odoo_proxmox_manager/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
proxmoxer>=1.3.1
|
||||
requests>=2.31.0
|
||||
9
odoo_proxmox_manager/security/ir.model.access.csv
Normal file
9
odoo_proxmox_manager/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_proxmox_server_user,proxmox.server.user,model_proxmox_server,group_proxmox_user,1,0,0,0
|
||||
access_proxmox_server_manager,proxmox.server.manager,model_proxmox_server,group_proxmox_manager,1,1,1,1
|
||||
access_proxmox_cluster_user,proxmox.cluster.user,model_proxmox_cluster,group_proxmox_user,1,0,0,0
|
||||
access_proxmox_cluster_manager,proxmox.cluster.manager,model_proxmox_cluster,group_proxmox_manager,1,1,1,1
|
||||
access_proxmox_vm_user,proxmox.vm.user,model_proxmox_vm,group_proxmox_user,1,1,0,0
|
||||
access_proxmox_vm_manager,proxmox.vm.manager,model_proxmox_vm,group_proxmox_manager,1,1,1,1
|
||||
access_proxmox_server_wizard,proxmox.server.wizard,model_proxmox_server_wizard,base.group_user,1,1,1,0
|
||||
access_proxmox_server_wizard_node,proxmox.server.wizard.node,model_proxmox_server_wizard_node,base.group_user,1,1,1,0
|
||||
|
39
odoo_proxmox_manager/security/proxmox_security.xml
Normal file
39
odoo_proxmox_manager/security/proxmox_security.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="module_proxmox_category" model="ir.module.category">
|
||||
<field name="name">Proxmox Management</field>
|
||||
<field name="description">Manage Proxmox servers and virtual machines</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="group_proxmox_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="category_id" ref="module_proxmox_category"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_proxmox_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="module_proxmox_category"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_proxmox_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="proxmox_server_rule" model="ir.rule">
|
||||
<field name="name">Proxmox Server Multi-Company</field>
|
||||
<field name="model_id" ref="model_proxmox_server"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<record id="proxmox_cluster_rule" model="ir.rule">
|
||||
<field name="name">Proxmox Cluster Multi-Company</field>
|
||||
<field name="model_id" ref="model_proxmox_cluster"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
BIN
odoo_proxmox_manager/static/description/icon.png
Normal file
BIN
odoo_proxmox_manager/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 833 B |
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
|
||||
class ProxmoxServerListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
}
|
||||
|
||||
async onAddServer() {
|
||||
await this.env.services.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'proxmox.server.wizard',
|
||||
view_mode: 'form',
|
||||
view_type: 'form',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ProxmoxServerListController.template = 'odoo_proxmox_manager.ProxmoxServerListView.Buttons';
|
||||
|
||||
export const proxmoxServerList = {
|
||||
...listView,
|
||||
Controller: ProxmoxServerListController,
|
||||
};
|
||||
|
||||
registry.category("views").add("proxmox_server_list", proxmoxServerList);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odoo_proxmox_manager.ProxmoxServerListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
|
||||
<button type="button" class="btn btn-primary" t-on-click="onAddServer">
|
||||
Ajouter un serveur
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
51
odoo_proxmox_manager/static/src/js/dashboard_view.js
Normal file
51
odoo_proxmox_manager/static/src/js/dashboard_view.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, onWillStart, useRef } from "@odoo/owl";
|
||||
|
||||
class ProxmoxDashboardController extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.serverData = {};
|
||||
this.chartRefs = {};
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const serverData = await this.orm.call(
|
||||
'proxmox.server',
|
||||
'get_dashboard_data',
|
||||
[]
|
||||
);
|
||||
this.serverData = serverData;
|
||||
this.render();
|
||||
}
|
||||
|
||||
openServers() {
|
||||
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_server');
|
||||
}
|
||||
|
||||
openClusters() {
|
||||
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_cluster');
|
||||
}
|
||||
|
||||
openVMs() {
|
||||
this.actionService.doAction('odoo_proxmox_manager.action_proxmox_vm');
|
||||
}
|
||||
}
|
||||
|
||||
ProxmoxDashboardController.template = 'odoo_proxmox_manager.DashboardView';
|
||||
ProxmoxDashboardController.components = { Layout };
|
||||
|
||||
registry.category("actions").add("proxmox_dashboard_client_action", {
|
||||
type: "ir.actions.client",
|
||||
tag: "proxmox_dashboard",
|
||||
target: "main",
|
||||
Component: ProxmoxDashboardController,
|
||||
});
|
||||
52
odoo_proxmox_manager/static/src/js/proxmox_server_list.js
Normal file
52
odoo_proxmox_manager/static/src/js/proxmox_server_list.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
|
||||
export class ProxmoxServerListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async getLocalState() {
|
||||
const state = await super.getLocalState();
|
||||
return { ...state, ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async setLocalState(state) {
|
||||
if (state) {
|
||||
this.state = { ...this.state, ...state };
|
||||
await super.setLocalState(state);
|
||||
}
|
||||
}
|
||||
|
||||
async onAddServer() {
|
||||
await this.env.services.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'proxmox.server.wizard',
|
||||
view_mode: 'form',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {},
|
||||
}, {
|
||||
onClose: () => this.actionService.restore(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ProxmoxServerListController.template = 'odoo_proxmox_manager.ProxmoxServerListView';
|
||||
|
||||
export const ProxmoxServerListView = {
|
||||
...listView,
|
||||
Controller: ProxmoxServerListController,
|
||||
};
|
||||
|
||||
registry.category("views").add("proxmox_server_list", ProxmoxServerListView);
|
||||
113
odoo_proxmox_manager/static/src/scss/dashboard.scss
Normal file
113
odoo_proxmox_manager/static/src/scss/dashboard.scss
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
.proxmox-dashboard {
|
||||
padding: 1rem;
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 2rem;
|
||||
h1 {
|
||||
color: #2b2b2b;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #2b2b2b;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-servers {
|
||||
h2 {
|
||||
color: #2b2b2b;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.server-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.server-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&.status-online {
|
||||
background: #e6f4ea;
|
||||
color: #1e8e3e;
|
||||
}
|
||||
|
||||
&.status-offline {
|
||||
background: #fce8e6;
|
||||
color: #d93025;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: bold;
|
||||
color: #2b2b2b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
odoo_proxmox_manager/static/src/xml/dashboard.xml
Normal file
57
odoo_proxmox_manager/static/src/xml/dashboard.xml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odoo_proxmox_manager.DashboardView" owl="1">
|
||||
<Layout>
|
||||
<div class="proxmox-dashboard">
|
||||
<div class="dashboard-header">
|
||||
<h1>Proxmox Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-card" t-on-click="openServers">
|
||||
<div class="stat-value"><t t-esc="serverData.server_count || 0"/></div>
|
||||
<div class="stat-label">Servers</div>
|
||||
</div>
|
||||
<div class="stat-card" t-on-click="openClusters">
|
||||
<div class="stat-value"><t t-esc="serverData.cluster_count || 0"/></div>
|
||||
<div class="stat-label">Clusters</div>
|
||||
</div>
|
||||
<div class="stat-card" t-on-click="openVMs">
|
||||
<div class="stat-value"><t t-esc="serverData.vm_count || 0"/></div>
|
||||
<div class="stat-label">Virtual Machines</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-servers">
|
||||
<h2>Server Status</h2>
|
||||
<div class="server-list">
|
||||
<t t-foreach="serverData.servers || []" t-as="server" t-key="server.id">
|
||||
<div class="server-card">
|
||||
<div class="server-header">
|
||||
<span class="server-name"><t t-esc="server.name"/></span>
|
||||
<span t-attf-class="server-status status-#{server.state}">
|
||||
<t t-esc="server.state"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="server-stats">
|
||||
<div class="stat">
|
||||
<span class="label">VMs:</span>
|
||||
<span class="value"><t t-esc="server.vm_count"/></span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Memory:</span>
|
||||
<span class="value"><t t-esc="server.memory_usage"/>%</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">CPU:</span>
|
||||
<span class="value"><t t-esc="server.cpu_usage"/>%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</t>
|
||||
</templates>
|
||||
10
odoo_proxmox_manager/static/src/xml/proxmox_server_list.xml
Normal file
10
odoo_proxmox_manager/static/src/xml/proxmox_server_list.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odoo_proxmox_manager.ProxmoxServerListView" t-inherit="web.ListView" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_control_panel_main_buttons')]" position="inside">
|
||||
<button type="button" class="btn btn-primary" t-on-click="onAddServer">
|
||||
Ajouter un serveur
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
89
odoo_proxmox_manager/views/proxmox_cluster_views.xml
Normal file
89
odoo_proxmox_manager/views/proxmox_cluster_views.xml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Cluster Tree View -->
|
||||
<record id="view_proxmox_cluster_tree" model="ir.ui.view">
|
||||
<field name="name">proxmox.cluster.tree</field>
|
||||
<field name="model">proxmox.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Proxmox Clusters" create="False">
|
||||
<field name="name"/>
|
||||
<field name="server_count"/>
|
||||
<field name="active_servers"/>
|
||||
<field name="total_vms"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Cluster Form View -->
|
||||
<record id="view_proxmox_cluster_form" model="ir.ui.view">
|
||||
<field name="name">proxmox.cluster.form</field>
|
||||
<field name="model">proxmox.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Proxmox Cluster">
|
||||
<header>
|
||||
<button name="action_sync_all_servers" string="Sync All Servers" type="object" class="oe_highlight"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Cluster Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="server_count"/>
|
||||
<field name="active_servers"/>
|
||||
<field name="total_vms"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Servers">
|
||||
<field name="server_ids">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="hostname"/>
|
||||
<field name="state"/>
|
||||
<field name="vm_count"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Cluster Search View -->
|
||||
<record id="view_proxmox_cluster_search" model="ir.ui.view">
|
||||
<field name="name">proxmox.cluster.search</field>
|
||||
<field name="model">proxmox.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Proxmox Clusters">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Cluster Action Window -->
|
||||
<record id="action_proxmox_cluster" model="ir.actions.act_window">
|
||||
<field name="name">Proxmox Clusters</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">proxmox.cluster</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_proxmox_cluster_search"/>
|
||||
<field name="context">{'form_view_ref': 'odoo_proxmox_manager.view_proxmox_cluster_form'}</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="limit">80</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first Proxmox cluster!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
32
odoo_proxmox_manager/views/proxmox_menus.xml
Normal file
32
odoo_proxmox_manager/views/proxmox_menus.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Main Menu -->
|
||||
<menuitem id="menu_proxmox_root"
|
||||
name="Proxmox"
|
||||
web_icon="odoo_proxmox_manager,static/description/icon.png"
|
||||
sequence="100"
|
||||
groups="base.group_system"/>
|
||||
|
||||
<!-- Clusters Menu -->
|
||||
<menuitem id="menu_proxmox_clusters"
|
||||
name="Clusters"
|
||||
parent="menu_proxmox_root"
|
||||
action="action_proxmox_cluster"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Servers Menu -->
|
||||
<menuitem id="menu_proxmox_servers"
|
||||
name="Servers"
|
||||
parent="menu_proxmox_root"
|
||||
action="action_proxmox_server"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Virtual Machines Menu -->
|
||||
<menuitem id="menu_proxmox_vms"
|
||||
name="Virtual Machines"
|
||||
parent="menu_proxmox_root"
|
||||
action="action_proxmox_vm"
|
||||
sequence="30"/>
|
||||
</data>
|
||||
</odoo>
|
||||
108
odoo_proxmox_manager/views/proxmox_server_views.xml
Normal file
108
odoo_proxmox_manager/views/proxmox_server_views.xml
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Server Tree View -->
|
||||
<record id="view_proxmox_server_tree" model="ir.ui.view">
|
||||
<field name="name">proxmox.server.tree</field>
|
||||
<field name="model">proxmox.server</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="proxmox_server_list" string="Proxmox Servers">
|
||||
<field name="name"/>
|
||||
<field name="hostname"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="state"/>
|
||||
<field name="vm_count"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server Form View -->
|
||||
<record id="view_proxmox_server_form" model="ir.ui.view">
|
||||
<field name="name">proxmox.server.form</field>
|
||||
<field name="model">proxmox.server</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Proxmox Server">
|
||||
<header>
|
||||
<button name="action_sync_vms" string="Sync VMs" type="object" class="oe_highlight"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Server Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="hostname"/>
|
||||
<field name="port"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="state"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="username"/>
|
||||
<field name="token_name"/>
|
||||
<field name="token_value" password="True"/>
|
||||
<field name="verify_ssl"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Virtual Machines">
|
||||
<field name="vm_ids">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="vmid"/>
|
||||
<field name="status"/>
|
||||
<field name="memory"/>
|
||||
<field name="cpus"/>
|
||||
<field name="disk_size"/>
|
||||
<field name="ip_address"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Node Information">
|
||||
<field name="node_info"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server Search View -->
|
||||
<record id="view_proxmox_server_search" model="ir.ui.view">
|
||||
<field name="name">proxmox.server.search</field>
|
||||
<field name="model">proxmox.server</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Proxmox Servers">
|
||||
<field name="name"/>
|
||||
<field name="hostname"/>
|
||||
<field name="cluster_id"/>
|
||||
<separator/>
|
||||
<filter string="Online" name="online" domain="[('state', '=', 'online')]"/>
|
||||
<filter string="Offline" name="offline" domain="[('state', '=', 'offline')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Cluster" name="cluster" context="{'group_by': 'cluster_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server Action Window -->
|
||||
<record id="action_proxmox_server" model="ir.actions.act_window">
|
||||
<field name="name">Proxmox Servers</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">proxmox.server</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_proxmox_server_search"/>
|
||||
<field name="context">{'search_default_cluster': 1, 'form_view_ref': 'odoo_proxmox_manager.view_proxmox_server_form'}</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="limit">80</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first Proxmox server!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
107
odoo_proxmox_manager/views/proxmox_vm_views.xml
Normal file
107
odoo_proxmox_manager/views/proxmox_vm_views.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- VM Tree View -->
|
||||
<record id="view_proxmox_vm_tree" model="ir.ui.view">
|
||||
<field name="name">proxmox.vm.tree</field>
|
||||
<field name="model">proxmox.vm</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Virtual Machines" create="False">
|
||||
<field name="name"/>
|
||||
<field name="vmid"/>
|
||||
<field name="server_id"/>
|
||||
<field name="status"/>
|
||||
<field name="memory"/>
|
||||
<field name="cpus"/>
|
||||
<field name="disk_size"/>
|
||||
<field name="ip_address"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- VM Form View -->
|
||||
<record id="view_proxmox_vm_form" model="ir.ui.view">
|
||||
<field name="name">proxmox.vm.form</field>
|
||||
<field name="model">proxmox.vm</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Virtual Machine">
|
||||
<header>
|
||||
<button name="action_start" string="Start" type="object"
|
||||
invisible="status == 'running'"
|
||||
class="oe_highlight"/>
|
||||
<button name="action_stop" string="Stop" type="object"
|
||||
invisible="status == 'stopped'"
|
||||
class="btn-danger"/>
|
||||
<button name="action_restart" string="Restart" type="object"
|
||||
invisible="status == 'stopped'"
|
||||
class="btn-warning"/>
|
||||
<button name="action_suspend" string="Suspend" type="object"
|
||||
invisible="status in ('suspended', 'stopped')"
|
||||
class="btn-info"/>
|
||||
<button name="action_resume" string="Resume" type="object"
|
||||
invisible="status != 'suspended'"
|
||||
class="oe_highlight"/>
|
||||
<field name="status" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="VM Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="vmid"/>
|
||||
<field name="server_id"/>
|
||||
<field name="ip_address"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="memory"/>
|
||||
<field name="cpus"/>
|
||||
<field name="disk_size"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- VM Search View -->
|
||||
<record id="view_proxmox_vm_search" model="ir.ui.view">
|
||||
<field name="name">proxmox.vm.search</field>
|
||||
<field name="model">proxmox.vm</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Virtual Machines">
|
||||
<field name="name"/>
|
||||
<field name="vmid"/>
|
||||
<field name="server_id"/>
|
||||
<field name="ip_address"/>
|
||||
<filter string="Running" name="running" domain="[('status', '=', 'running')]"/>
|
||||
<filter string="Stopped" name="stopped" domain="[('status', '=', 'stopped')]"/>
|
||||
<filter string="Suspended" name="suspended" domain="[('status', '=', 'suspended')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Server" name="server" context="{'group_by': 'server_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'status'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- VM Action Window -->
|
||||
<record id="action_proxmox_vm" model="ir.actions.act_window">
|
||||
<field name="name">Virtual Machines</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">proxmox.vm</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_proxmox_vm_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No virtual machines found!
|
||||
</p>
|
||||
<p>
|
||||
Virtual machines will be synchronized from your Proxmox servers.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
1
odoo_proxmox_manager/wizard/__init__.py
Normal file
1
odoo_proxmox_manager/wizard/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import proxmox_server_wizard
|
||||
98
odoo_proxmox_manager/wizard/proxmox_server_wizard.py
Normal file
98
odoo_proxmox_manager/wizard/proxmox_server_wizard.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
from odoo import api, fields, models
|
||||
import requests
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
class ProxmoxServerWizard(models.TransientModel):
|
||||
_name = 'proxmox.server.wizard'
|
||||
_description = 'Assistant de configuration serveur Proxmox'
|
||||
|
||||
name = fields.Char(string='Nom', required=True)
|
||||
hostname = fields.Char(string='Nom d\'hôte/IP', required=True)
|
||||
port = fields.Integer(string='Port', default=443)
|
||||
username = fields.Char(string='Nom d\'utilisateur', required=True)
|
||||
password = fields.Char(string='Mot de passe', required=True)
|
||||
verify_ssl = fields.Boolean(string='Vérifier SSL', default=False)
|
||||
|
||||
def _get_proxmox_connection(self):
|
||||
"""Établit une connexion à l'API Proxmox"""
|
||||
base_url = f"https://{self.hostname}:{self.port}/api2/json"
|
||||
try:
|
||||
auth_response = requests.post(
|
||||
f"{base_url}/access/ticket",
|
||||
verify=self.verify_ssl,
|
||||
data={
|
||||
"username": self.username,
|
||||
"password": self.password
|
||||
}
|
||||
)
|
||||
auth_response.raise_for_status()
|
||||
auth_data = auth_response.json()['data']
|
||||
|
||||
headers = {
|
||||
'CSRFPreventionToken': auth_data['CSRFPreventionToken'],
|
||||
'Cookie': f"PVEAuthCookie={auth_data['ticket']}"
|
||||
}
|
||||
return base_url, headers
|
||||
except Exception as e:
|
||||
raise ValueError(f"Erreur de connexion: {str(e)}")
|
||||
|
||||
def action_test_connection(self):
|
||||
"""Teste la connexion et crée le serveur et le cluster si la connexion réussit"""
|
||||
try:
|
||||
base_url, headers = self._get_proxmox_connection()
|
||||
|
||||
# Vérifier si c'est un cluster
|
||||
cluster_response = requests.get(
|
||||
f"{base_url}/cluster/status",
|
||||
headers=headers,
|
||||
verify=self.verify_ssl
|
||||
)
|
||||
cluster_response.raise_for_status()
|
||||
cluster_data = cluster_response.json()['data']
|
||||
|
||||
is_cluster = any(node['type'] == 'cluster' for node in cluster_data)
|
||||
cluster = False
|
||||
|
||||
if is_cluster:
|
||||
cluster_name = next((node['name'] for node in cluster_data if node['type'] == 'cluster'), 'Cluster Proxmox')
|
||||
cluster = self.env['proxmox.cluster'].create({
|
||||
'name': cluster_name,
|
||||
})
|
||||
|
||||
# Créer le serveur principal
|
||||
main_server = self.env['proxmox.server'].create({
|
||||
'name': self.name,
|
||||
'hostname': self.hostname,
|
||||
'port': self.port,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'cluster_id': cluster.id if cluster else False,
|
||||
})
|
||||
|
||||
# Créer les autres nœuds si c'est un cluster
|
||||
if is_cluster:
|
||||
nodes = [node for node in cluster_data if node['type'] == 'node' and node['name'] != self.hostname]
|
||||
for node in nodes:
|
||||
self.env['proxmox.server'].create({
|
||||
'name': node['name'],
|
||||
'hostname': node['name'],
|
||||
'port': self.port,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'cluster_id': cluster.id,
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'proxmox.server',
|
||||
'res_id': main_server.id,
|
||||
'view_mode': 'form',
|
||||
'view_type': 'form',
|
||||
'views': [(False, 'form')],
|
||||
'target': 'current',
|
||||
'flags': {'mode': 'readonly'},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Erreur lors du test de connexion: {str(e)}")
|
||||
67
odoo_proxmox_manager/wizard/proxmox_server_wizard_views.xml
Normal file
67
odoo_proxmox_manager/wizard/proxmox_server_wizard_views.xml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_proxmox_server_wizard_form" model="ir.ui.view">
|
||||
<field name="name">proxmox.server.wizard.form</field>
|
||||
<field name="model">proxmox.server.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuration Serveur Proxmox">
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group states="connect">
|
||||
<group>
|
||||
<field name="name" placeholder="Ex: Proxmox-1"/>
|
||||
<field name="hostname" placeholder="Ex: proxmox.example.com"/>
|
||||
<field name="port"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="username" placeholder="Ex: root@pam"/>
|
||||
<field name="password" password="True"/>
|
||||
<field name="verify_ssl"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group states="config" attrs="{'invisible': [('state', '!=', 'config')]}">
|
||||
<group>
|
||||
<field name="is_cluster"/>
|
||||
<field name="cluster_name" attrs="{'invisible': [('is_cluster', '=', False)], 'required': [('is_cluster', '=', True)]}"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('is_cluster', '=', False)]}">
|
||||
<field name="other_nodes" nolabel="1" colspan="2">
|
||||
<tree editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="hostname"/>
|
||||
<field name="port"/>
|
||||
<field name="username"/>
|
||||
<field name="password" password="True"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_test_connection" string="Tester la connexion" type="object"
|
||||
class="btn-primary" states="connect"/>
|
||||
<button name="action_create_server" string="Créer" type="object"
|
||||
class="btn-primary" states="config"/>
|
||||
<button string="Annuler" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_proxmox_server_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Ajouter un serveur Proxmox</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">proxmox.server.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_proxmox_server_wizard"
|
||||
name="Ajouter un serveur"
|
||||
action="action_proxmox_server_wizard"
|
||||
parent="menu_proxmox_server_root"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
3
project_task_date_deadline_cascade_update/__init__.py
Normal file
3
project_task_date_deadline_cascade_update/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
19
project_task_date_deadline_cascade_update/__manifest__.py
Normal file
19
project_task_date_deadline_cascade_update/__manifest__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
'name': 'Project Task Date Deadline Cascade Update',
|
||||
'version': '17.0.0.1',
|
||||
'category': 'Project',
|
||||
'summary': 'Automatically update child task deadlines when parent task deadline changes.',
|
||||
'description': """
|
||||
This module ensures that when a parent task's deadline is updated, all child tasks' deadlines are automatically updated to match.
|
||||
""",
|
||||
'author': 'Your Name',
|
||||
'license': 'AGPL-3',
|
||||
'website': 'https://www.yourwebsite.com',
|
||||
'depends': ['project'],
|
||||
'data': [
|
||||
'views/update_deadline_wizard_view.xml',
|
||||
'wizard/update_deadline_wizard.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import project_task
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import _
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
@api.model
|
||||
def write(self, vals):
|
||||
res = super(ProjectTask, self).write(vals)
|
||||
if 'date_deadline' in vals:
|
||||
for task in self:
|
||||
if task.child_ids:
|
||||
return {
|
||||
'name': _('Update Deadline'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'update.deadline.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_ids': self.ids},
|
||||
}
|
||||
return res
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_update_deadline_wizard_form" model="ir.ui.view">
|
||||
<field name="name">update.deadline.wizard.form</field>
|
||||
<field name="model">update.deadline.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Update Deadline">
|
||||
<group>
|
||||
<field name="apply_to_children"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Apply" type="object" name="update_deadline" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_update_deadline_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Update Deadline</field>
|
||||
<field name="res_model">update.deadline.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_update_deadline_wizard_form"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import update_deadline_wizard
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class UpdateDeadlineWizard(models.TransientModel):
|
||||
_name = 'update.deadline.wizard'
|
||||
_description = 'Update Deadline Wizard'
|
||||
|
||||
apply_to_children = fields.Boolean(string='Apply to Child Tasks', default=False)
|
||||
|
||||
def update_deadline(self):
|
||||
context = dict(self._context or {})
|
||||
active_ids = context.get('active_ids', [])
|
||||
tasks = self.env['project.task'].browse(active_ids)
|
||||
for task in tasks:
|
||||
if task.child_ids and self.apply_to_children:
|
||||
task.child_ids.write({'date_deadline': task.date_deadline})
|
||||
Loading…
Reference in a new issue